In [1]:
import warnings
warnings.filterwarnings('ignore')
In [2]:
import pandas as pd
import numpy as np
from datetime import date, datetime, timedelta
import matplotlib.pyplot as plt
import seaborn as sns
from statistics import mode
import scipy.stats as stats
In [3]:
## CONSTANTES ##
FILE_BINANCE = 'BTCUSDT_binance.csv'
FILE_BINANCEAPI = 'BTCUSDT_binanceapi.csv'
FILE_COINMARKETCAP = 'BTCUSD_coinmarketcap.csv'
FILE_BTCDIRECT = 'BTCUSD_btcdirect.csv'
FILE_YAHOOFINANCE = 'BTCUSD_yahoofinance.csv'
FILE_COINGECKO = 'BTCUSD_coingecko.txt'
FILE_FEDERALFUNDS = 'FRB_H15.csv'
FILE_SP500 = 'SP500.csv'
FILE_NASDAQ = 'NASDAQ.csv'
FILE_EUR_USD = 'EURUSD.csv'
FILE_GBP_USD = 'GBPUSD.csv'
FILE_JPY_USD = 'JPYUSD.csv'
In [4]:
## FUNCIONES ##

# Devuelve el mínimo y máximo de una variable dada
def var_min_max(_df, _var):
    return (_df[_var].min(), _df[_var].max())
    

# Devuelve un dataframe con información de análisis univariante de variables cuantitativas continuas:
def univar_num_cont(_df, _list_var):
    univar_data = []
    for _var in _list_var:
        univar_data.append([_var, _df[_var].nunique(), round(_df[_var].mean(),2), round(_df[_var].median(),2), 
                            round(_df[_var].min(),2), round(_df[_var].max(),2), round(np.sqrt(np.var(_df[_var])),2)])
    return pd.DataFrame(univar_data, columns=["Variable", "Valores Únicos", "Media", "Mediana", "Mínimo", 
                                              "Máximo", "Desv. Estándar"])


# Devuelve un dataframe con información de análisis univariante de variables cuantitativas discretas:
def univar_num_disc(_df, _list_var):
    univar_data = []
    for _var in _list_var:
        univar_data.append([_var, _df[_var].min(), round(np.percentile(_df[_var], (25)),2), _df[_var].median(), 
                            round(np.percentile(_df[_var], (75)),2), _df[_var].max(), mode(_df[_var]), 
                            round(np.sqrt(np.var(_df[_var])),2)])
    return pd.DataFrame(univar_data, columns=["Variable",  "Mínimo", "Perc25", "Mediana", "Perc75", "Máximo", 
                                              "Moda",  "Desv. Estándar"])


# Rellena valores nulos de una variable con su moda
def impute_mode(_df, _var):
    return _df[_var].fillna(mode(_df[_var]))


# Rellena valores nulos de una variable con el valor no nulo inmediatamente anterior (u otra forma si se modifica _method)
def impute_forward_fill(_df, _var, _method='ffill'):
    return _df[_var].fillna(method=_method)


# Rellena los valores nulos de una variable mediante interpolación lineal (u otros si se modifica _method)
def impute_interpolate(_df, _var, _method='linear', _limit_direction='forward', _axis=0):
    return _df[_var].interpolate(method=_method, limit_direction=_limit_direction, axis=_axis)


# Para realizar una normalización z-score (incluye opción para realizarla en ventanas de tiempo)
def normalize_zscore(_df, _var, _window=0):
    if(_window>0):
        return _df[_var].rolling(window=_window, min_periods=1).apply(
            lambda x: (x.iloc[-1] - x.mean()) / x.std(ddof=0) if x.std(ddof=0) > 0 else 0, raw=False)
    else:
        return (_df[_var] - _df[_var].mean()) / _df[_var].std(ddof=0)
    
    
# Devuelve un valor desnormalizado; emplear cuando el modelo haya dado una predicción de precio para conocer el valor en USDT
def denormalize_data(_data, _df, _var='close', _window=20, _index=None):
    if _index is None:
        _index = len(_df) - 1
        
    serie = _df[_var]

    # Calcular la media y desviación estándar de la ventana en el índice de predicción
    if _index >= _window - 1:  # Si el índice tiene suficientes datos para la ventana completa
        ventana = serie.iloc[_index - _window + 1 : _index + 1]
    else:  # Si el índice está en el rango inicial y usa una ventana reducida
        ventana = serie.iloc[: _index + 1]

    mean_window = ventana.mean()
    std_window = ventana.std(ddof=0)

    # Desnormalizar el valor
    if std_window > 0:  # Evitar división por cero
        return round((_data * std_window) + mean_window, 2)
    else:
        return round(mean_window, 2)  # Si la desviación es 0, el valor normalizado corresponde a la media


# Para estandarizar el dataset original con el que se entrena y valida el modelo y posteriores datasets
def standard_dataset(_x, _var_list):
    scaler = StandardScaler()
    
    for var in _var_list:
        _x[var] = scaler.fit_transform(_x[[var]])

    return _x


# Cálculo del R2 ajustado
def adjusted_r2(_r2, _n, _p):
    return 1 - ((1 - _r2) * (_n - 1) / (_n - _p - 1))
In [5]:
## INDICADORES TÉCNICOS ##

# SMA
def sma(_df, _window=7):
    return _df['close'].rolling(window=_window).mean()


# RSI
def rsi(_df, _window=14):
    delta = _df['close'].diff()
    gain = delta.where(delta > 0, 0)
    loss = -delta.where(delta < 0, 0)

    avg_gain = gain.rolling(window=_window).mean()
    avg_loss = loss.rolling(window=_window).mean()

    rsi = avg_gain / avg_loss
    return (100 - (100 / (1 + rsi)))


# Bandas de Bollinger
def bollinger_bands(_df, _sma=20):
    return _df['sma_20'] + 2 * _df['close'].rolling(window=_sma).std(), \
            _df['sma_20'] - 2 * _df['close'].rolling(window=_sma).std()


# MACD
def macd(_df, _ema1=12, _ema2=26, _signal_length=9):
    ema_12 = _df['close'].ewm(span=_ema1, adjust=False).mean()
    ema_26 = _df['close'].ewm(span=_ema2, adjust=False).mean()
    macd = ema_12 - ema_26

    return macd, macd.ewm(span=_signal_length, adjust=False).mean()


# Volatilidad histórica
def historical_volatility(_df, _window=20):
    log_returns = np.log(_df['close'] / _df['close'].shift(1))
    return log_returns.rolling(window=_window).std()
In [6]:
## GRÁFICOS ##
# Representa un histograma con líneas de mediana y media para una variable cuantitativa continua
def univar_graph_num_cont(_df, _var, _bins=False):
    if _bins == False:
        _bins = 20
    
    media = _df[_var].mean()
    mediana = _df[_var].median()
    
    plt.figure(figsize=(10,6))
    plt.axvline(mediana, color='orange', linestyle='-', label=f'Mediana: {mediana:.2f}')
    plt.axvline(media, color='black', linestyle='-', label=f'Media: {media:.2f}')
    sns.histplot(_df[_var], bins=_bins, kde=True)
    
    plt.xlabel(_var)
    plt.ylabel("Frecuencia")
    plt.title(f"Histograma de '{_var}'")
    
    plt.legend()
    plt.show()
    
    
# Representa un gráfico de barras para una variable cuantitativa discreta
def univar_graph_num_disc(_df, _var):
    values = _df[_var].value_counts().sort_index()
    
    plt.figure(figsize=(10, 6))
    values.plot(kind='bar', color='skyblue')
    plt.title(f"Distribución de '{_var}'")
    plt.ylabel("Frecuencia")
    plt.xlabel(f"{_var}")
    plt.xticks(rotation=65)
    
    plt.show()
    
    
# Representa un gráfico boxplot de la distribución de una variable
def univar_graph_boxplot(_df, _var):
    plt.figure(figsize=(12, 6))
    sns.boxplot(x= df[_var])
    plt.xticks(rotation=65)
    plt.title(f'Boxplot de {_var}')
    
    plt.show()
    
    
# Representa un scatterplot de comparación entre una variable cuantitativa y la variable objetivo 
def bivar_graph_scatt(_df, _var, _obj_var='close'):
    plt.figure(figsize=(10, 6))
    sns.regplot(x=_var, y=_obj_var, data=_df, scatter_kws={"s": 20}, line_kws={"color": "red"})
    plt.title(f"Comparación de {_var} y {_obj_var}")
    plt.xlabel(_var)
    plt.ylabel(_obj_var)
    
    plt.show()
    
    
# Representa la evolución en el tiempo de una variable
def time_line(_df, _var_date='date', _vars=['close']):
    plt.figure(figsize=(12, 6))
    
    for _var in _vars:
        sns.lineplot(data=_df, x=_var_date, y=_var, label=f"'{_var}'")
    
    plt.title(f"Evolución de '{_vars}' en el tiempo", fontsize=16)
    plt.xlabel('Tiempo', fontsize=12)
    plt.ylabel(_vars, fontsize=12)
    plt.xticks(rotation=45)
    plt.legend()
    plt.grid(True, linestyle='--', alpha=0.5)
    plt.show()

    
# Representa la matriz de correlación de las variables numéricas del dataframe
def multivar_graph_correlation_matrix(_df, _method='pearson'):
    corr = _df.corr(method=_method)
    mask = np.triu(np.ones_like(corr, dtype=bool))
    
    f, ax = plt.subplots(figsize=(11, 9))
    cmap = sns.diverging_palette(230, 20, as_cmap=True)
    sns.set_theme(style="white")
    sns.heatmap(corr, mask=mask, cmap=cmap, vmax=1, vmin=-1, center=0,
                square=True, linewidths=.5, cbar_kws={"shrink": .5});
    

# Representa un gráfico comparando cada predicción con su residuo
def residuals_vs_predictions(_pred, _resid):
    plt.figure(figsize=(14, 6))

    plt.scatter(_pred, _resid, color='orange', alpha=0.6)
    plt.axhline(0, color='black', linestyle='--')
    plt.title("Residuos vs Predicciones")
    plt.xlabel("Predicciones")
    plt.ylabel("Residuos")

    
# Representa un gráfico comparando los valores de test con los valores predichos por el modelo
def test_vs_estimacion(_y_test, _y_pred):
    fig, ax = plt.subplots(figsize=(12,8))
    plt.scatter(_y_test, _y_pred,  color='royalblue')
    plt.plot(_y_test, _y_test, color='black', linewidth=3)

    plt.xlabel("Test")
    plt.ylabel("Estimación")
    plt.show()
    

# Representa un histograma de distribución de residuos
def residual_histogram(_residuals, _bins=20):
    plt.figure(figsize=(14, 6))

    plt.hist(_residuals, bins=_bins, color='orange', alpha=1)
    plt.title("Histograma de Residuos")
    plt.xlabel("Residuos")

    plt.tight_layout()
    plt.show()
    
    
# Representa un gráfico QQPlot para comparar la normalidad de los residuos
def residuals_qqplot(_residuals):
    plt.figure(figsize=(14, 6))

    stats.probplot(_residuals, dist="norm", plot=plt)
    plt.title("QQPlot de Residuos")

    plt.tight_layout()
    plt.show()
In [7]:
## FICHEROS ##

# Guardar y recuperar datos csv.
def save_csv(_df, _coin1, _coin2, _date=False):
    pair = _coin1 + _coin2
    if(_date):
        date = _df.date[0].strftime('%Y%m%d')
        _df.to_csv(f'archivos/{pair}_{date}.csv', index=False)
    else:
        _df.to_csv(f'archivos/{pair}.csv', index=False)
    
def open_csv(_file):
    return pd.read_csv(f'archivos/{_file}')


def open_json_file(_file):
    with open(f'archivos/{_file}', 'r', encoding='utf-8') as file:
        return json.load(file)

2.2. Colección inicial de datos¶

In [8]:
df_data_coinmarketcap = open_csv(FILE_COINMARKETCAP)
df_data_binanceapi = open_csv(FILE_BINANCEAPI)
df_data_federalfunds = open_csv(FILE_FEDERALFUNDS)
df_data_sp500 = open_csv(FILE_SP500)
df_data_nasdaq = open_csv(FILE_NASDAQ)
df_eur_usd = open_csv(FILE_EUR_USD)
df_gbp_usd = open_csv(FILE_GBP_USD)
df_jpy_usd = open_csv(FILE_JPY_USD)

El dataset inicial será el que contiene los datos de la API de Binance, es decir, df_data_binanceapi. A partir de él, se tratará de añadir nuevos datos del resto de conjuntos de datos.

In [9]:
df_inicial = df_data_binanceapi.copy()
df_inicial
Out[9]:
date open high low close VOL_BTC VOL_USDT transactions buy_market_BTC buy_market_USDT
0 2017-08-17 4261.48 4485.39 4200.74 4285.08 795.150377 3.454770e+06 3427 616.248541 2.678216e+06
1 2017-08-18 4285.08 4371.52 3938.77 4108.37 1199.888264 5.086958e+06 5233 972.868710 4.129123e+06
2 2017-08-19 4108.37 4184.69 3850.00 4139.98 381.309763 1.549484e+06 2153 274.336042 1.118002e+06
3 2017-08-20 4120.98 4211.08 4032.62 4086.29 467.083022 1.930364e+06 2321 376.795947 1.557401e+06
4 2017-08-21 4069.13 4119.62 3911.79 4016.00 691.743060 2.797232e+06 3972 557.356107 2.255663e+06
... ... ... ... ... ... ... ... ... ... ...
2655 2024-11-23 98892.00 98908.85 97136.00 97672.40 24757.843670 2.431610e+09 3839138 11176.248700 1.097539e+09
2656 2024-11-24 97672.40 98564.00 95734.77 97900.04 31200.978380 3.034172e+09 4964720 15124.121760 1.471314e+09
2657 2024-11-25 97900.05 98871.80 92600.19 93010.01 50847.450960 4.883445e+09 8289691 23486.927050 2.255517e+09
2658 2024-11-26 93010.01 94973.37 90791.10 91965.16 57858.731380 5.370919e+09 10225809 29120.267650 2.702544e+09
2659 2024-11-27 91965.16 96539.33 91792.14 96316.00 32915.047770 3.100202e+09 5553374 16124.386890 1.518456e+09

2660 rows × 10 columns

Se descarta la observación para la fecha 27/11/2024 por no ser un día cerrado, por lo que, excepto el precio de apertura, el resto de campos son susceptibles de sufrir variaciones en su valor.

In [10]:
df_inicial = df_inicial[df_inicial['date'] != '2024-11-27']
df_inicial.tail()
Out[10]:
date open high low close VOL_BTC VOL_USDT transactions buy_market_BTC buy_market_USDT
2654 2024-11-22 98317.12 99588.01 97122.11 98892.00 46189.309243 4.555537e+09 7271311 22709.668193 2.240635e+09
2655 2024-11-23 98892.00 98908.85 97136.00 97672.40 24757.843670 2.431610e+09 3839138 11176.248700 1.097539e+09
2656 2024-11-24 97672.40 98564.00 95734.77 97900.04 31200.978380 3.034172e+09 4964720 15124.121760 1.471314e+09
2657 2024-11-25 97900.05 98871.80 92600.19 93010.01 50847.450960 4.883445e+09 8289691 23486.927050 2.255517e+09
2658 2024-11-26 93010.01 94973.37 90791.10 91965.16 57858.731380 5.370919e+09 10225809 29120.267650 2.702544e+09

Los datasets obtenidos mediante scraping a la web de Binance, BtcDirect, Yahoo Finance y CoinGecko, serán descartados, el primero por ser igual pero con menor número de observaciones que el obtenido con la librería de Binance para Python, y los demás por aportar tan sólo valores de precio y volumen, datos que ya posee el conjunto que se ha escogido como inicial.

Por otro lado, del dataset obtenido haciendo scraping de CoinMarketCap, contiene dos variables extra que no posee el dataset inicial: la capitalización de mercado y el supply total de Bitcoin. Dado que la capitalización de mercado resulta de multiplicar el supply por el precio, incluir ambas variables en el dataset inicial supondría que, al realizar el análisis EDA, aparecería un problema de colinealidad perfecta que llevaría a tener que eliminar la variable en cuestión. Por ello, se toma la decisión de incluir en el dataset únicamente la variable total_supply y descartar market_cap.

Además, hay que tener en consideración que, para el dataset de CoinMarketCap tenemos únicamente una observación por semana y que comienza en 14/07/2010, por lo que habrá que eliminar todas las observaciones para fechas anteriores a 17/08/2017 e idear una estrategia para interpolar los datos para todas aquellas fechas que no lo poseen.

Dado que el supply total de Bitcoin es acumulativo y, por tanto sólo puede aumentar o permanecer constante en el tiempo (podría reducirse de forma excepcional por diversas razones, pero es poco probable), una estrategia aceptable podría ser rellenar los datos faltantes mediante una interpolación lineal.

In [11]:
df_coinmarketcap_modified = df_data_coinmarketcap.copy()

# Eliminación de las observaciones anteriores a 17/08/2017
df_coinmarketcap_modified = df_coinmarketcap_modified[df_coinmarketcap_modified['date'] >= "2017-08-17"]

# Se añaden las fechas para las que no existe observación
dates = pd.date_range(start='2017-08-17', end=df_inicial['date'].max())
df_dates = pd.DataFrame({'date': dates})
df_coinmarketcap_modified['date'] = pd.to_datetime(df_coinmarketcap_modified['date'])
df_coinmarketcap_modified = pd.merge(df_dates, df_coinmarketcap_modified, on='date', how='left')

# Interpolación de los datos faltantes de total_supply
df_coinmarketcap_modified['total_supply'] = df_coinmarketcap_modified['total_supply'].interpolate(method='linear')

# Extrapolación de los datos faltantes desde la primera fecha de la que se tiene valor para total_supply hasta el 17/28/2017
df_coinmarketcap_modified['total_supply'].fillna(method='bfill', inplace=True)

# Unión de la variable total_supply al dataset inicial
df_inicial['date'] = pd.to_datetime(df_inicial['date'])
df_inicial = pd.merge(df_inicial, df_coinmarketcap_modified[['date', 'total_supply']], on='date', how='left')

df_inicial
Out[11]:
date open high low close VOL_BTC VOL_USDT transactions buy_market_BTC buy_market_USDT total_supply
0 2017-08-17 4261.48 4485.39 4200.74 4285.08 795.150377 3.454770e+06 3427 616.248541 2.678216e+06 1.651956e+07
1 2017-08-18 4285.08 4371.52 3938.77 4108.37 1199.888264 5.086958e+06 5233 972.868710 4.129123e+06 1.651956e+07
2 2017-08-19 4108.37 4184.69 3850.00 4139.98 381.309763 1.549484e+06 2153 274.336042 1.118002e+06 1.651956e+07
3 2017-08-20 4120.98 4211.08 4032.62 4086.29 467.083022 1.930364e+06 2321 376.795947 1.557401e+06 1.651956e+07
4 2017-08-21 4069.13 4119.62 3911.79 4016.00 691.743060 2.797232e+06 3972 557.356107 2.255663e+06 1.651956e+07
... ... ... ... ... ... ... ... ... ... ... ...
2654 2024-11-22 98317.12 99588.01 97122.11 98892.00 46189.309243 4.555537e+09 7271311 22709.668193 2.240635e+09 1.978572e+07
2655 2024-11-23 98892.00 98908.85 97136.00 97672.40 24757.843670 2.431610e+09 3839138 11176.248700 1.097539e+09 1.978626e+07
2656 2024-11-24 97672.40 98564.00 95734.77 97900.04 31200.978380 3.034172e+09 4964720 15124.121760 1.471314e+09 1.978680e+07
2657 2024-11-25 97900.05 98871.80 92600.19 93010.01 50847.450960 4.883445e+09 8289691 23486.927050 2.255517e+09 1.978734e+07
2658 2024-11-26 93010.01 94973.37 90791.10 91965.16 57858.731380 5.370919e+09 10225809 29120.267650 2.702544e+09 1.978734e+07

2659 rows × 11 columns

Se obtiene finalmente un dataset inicial con las siguientes variables:

  • date: fecha de la "vela" (considerando que los diferentes niveles de precio hacen referencia a una candelstick o vela); se considera como las 24 horas en las que se produce la vela.
  • open: precio de apertura del día.
  • high: mayor precio alcanzado en el día.
  • low: menor precio alcanzado en el día.
  • close: precio de cierre del día.
  • _VOLBTC: volumen de BTC negociados en el día.
  • _VOLUSDT: volumen de USDT negociados en el día.
  • transactions: número de transacciones completadas en el día.
  • _buy_marketBTC: número de BTC comprados a mercado en el día.
  • _buy_marketUSDT: número de USDT comprados a mercado en el día.
  • _totalsupply: suministro circulante de BTC en el día.

A continuación, se agregan datos adicionales:

  • _fed_fundsrate: tasa de interés de los fondos federales de Estados Unidos del día.
  • sp500: valor de cierre del SP500.
  • nasdaq: valor de cierre del NASDAQ.
  • _eurusd: valor de cierre del par EURO/DÓLAR.
  • _gbpusd: valor de cierre del par LIBRA/DÓLAR.
  • _jpyusd: valor de cierre del par YEN/DÓLAR.
In [12]:
df_inicial.columns
Out[12]:
Index(['date', 'open', 'high', 'low', 'close', 'VOL_BTC', 'VOL_USDT',
       'transactions', 'buy_market_BTC', 'buy_market_USDT', 'total_supply'],
      dtype='object')
In [13]:
# Tasa de interés de los fondos federales de Estados Unidos
df_data_federalfunds.columns
Out[13]:
Index(['Time Period', 'RIFSPFF_N.D'], dtype='object')
In [14]:
df_data_federalfunds.rename(columns={'Time Period': 'date', 'RIFSPFF_N.D': 'fed_funds_rate'}, inplace=True)
df_data_federalfunds = df_data_federalfunds[df_data_federalfunds['date'] >= "2017-08-17"]
df_data_federalfunds['date'] = pd.to_datetime(df_data_federalfunds['date'])
df_data_federalfunds
Out[14]:
date fed_funds_rate
228 2017-08-17 1.16
229 2017-08-18 1.16
230 2017-08-19 1.16
231 2017-08-20 1.16
232 2017-08-21 1.16
... ... ...
2882 2024-11-22 4.58
2883 2024-11-23 4.58
2884 2024-11-24 4.58
2885 2024-11-25 4.58
2886 2024-11-26 4.58

2659 rows × 2 columns

In [15]:
# Datos de precio de SP500
df_data_sp500.columns
Out[15]:
Index(['Date', 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume'], dtype='object')
In [16]:
df_data_sp500 = df_data_sp500.rename(columns={'Date': 'date', 'Close': 'sp500'})[['date', 'sp500']]
df_data_sp500 = df_data_sp500[df_data_sp500['date'] >= "2017-08-17"]
df_data_sp500['date'] = pd.to_datetime(df_data_sp500['date'])
df_data_sp500
Out[16]:
date sp500
1085 2017-08-17 2430.010010
1086 2017-08-18 2425.550049
1087 2017-08-21 2428.370117
1088 2017-08-22 2452.510010
1089 2017-08-23 2444.040039
... ... ...
2912 2024-11-20 5917.109863
2913 2024-11-21 5948.709961
2914 2024-11-22 5969.339844
2915 2024-11-25 5987.370117
2916 2024-11-26 6021.629883

1832 rows × 2 columns

In [17]:
# Datos de precio de NASDAQ
df_data_nasdaq.columns
Out[17]:
Index(['Date', 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume'], dtype='object')
In [18]:
df_data_nasdaq = df_data_nasdaq.rename(columns={'Date': 'date', 'Close': 'nasdaq'})[['date', 'nasdaq']]
df_data_nasdaq = df_data_nasdaq[df_data_nasdaq['date'] >= "2017-08-17"]
df_data_nasdaq['date'] = pd.to_datetime(df_data_nasdaq['date'])
df_data_nasdaq
Out[18]:
date nasdaq
1085 2017-08-17 6221.910156
1086 2017-08-18 6216.529785
1087 2017-08-21 6213.129883
1088 2017-08-22 6297.479980
1089 2017-08-23 6278.410156
... ... ...
2912 2024-11-20 18966.140625
2913 2024-11-21 18972.419922
2914 2024-11-22 19003.650391
2915 2024-11-25 19054.839844
2916 2024-11-26 19174.300781

1832 rows × 2 columns

In [19]:
# Tipo de cambio Euro/Dólar
df_eur_usd.columns
Out[19]:
Index(['Date', 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume'], dtype='object')
In [20]:
df_eur_usd = df_eur_usd.rename(columns={'Date': 'date', 'Close': 'eur_usd'})[['date', 'eur_usd']]
df_eur_usd = df_eur_usd[df_eur_usd['date'] >= "2017-08-17"]
df_eur_usd['date'] = pd.to_datetime(df_eur_usd['date'])
df_eur_usd
Out[20]:
date eur_usd
1121 2017-08-17 1.177426
1122 2017-08-18 1.171550
1123 2017-08-21 1.175627
1124 2017-08-22 1.181195
1125 2017-08-23 1.176221
... ... ...
3013 2024-11-20 1.060760
3014 2024-11-21 1.054619
3015 2024-11-22 1.046934
3016 2024-11-25 1.047987
3017 2024-11-26 1.044430

1897 rows × 2 columns

In [21]:
# Tipo de cambio Libra/Dólar
df_gbp_usd = df_gbp_usd.rename(columns={'Date': 'date', 'Close': 'gbp_usd'})[['date', 'gbp_usd']]
df_gbp_usd = df_gbp_usd[df_gbp_usd['date'] >= "2017-08-17"]
df_gbp_usd['date'] = pd.to_datetime(df_gbp_usd['date'])
df_gbp_usd
Out[21]:
date gbp_usd
1121 2017-08-17 1.289158
1122 2017-08-18 1.286505
1123 2017-08-21 1.287366
1124 2017-08-22 1.289823
1125 2017-08-23 1.282397
... ... ...
3013 2024-11-20 1.269454
3014 2024-11-21 1.265534
3015 2024-11-22 1.258479
3016 2024-11-25 1.259382
3017 2024-11-26 1.253306

1897 rows × 2 columns

In [22]:
# Tipo de cambio Yen/Dólar
df_jpy_usd = df_jpy_usd.rename(columns={'Date': 'date', 'Close': 'jpy_usd'})[['date', 'jpy_usd']]
df_jpy_usd = df_jpy_usd[df_jpy_usd['date'] >= "2017-08-17"]
df_jpy_usd['date'] = pd.to_datetime(df_jpy_usd['date'])
df_jpy_usd
Out[22]:
date jpy_usd
1121 2017-08-17 0.009091
1122 2017-08-18 0.009147
1123 2017-08-21 0.009147
1124 2017-08-22 0.009179
1125 2017-08-23 0.009113
... ... ...
3013 2024-11-20 0.006463
3014 2024-11-21 0.006439
3015 2024-11-22 0.006484
3016 2024-11-25 0.006487
3017 2024-11-26 0.006478

1897 rows × 2 columns

In [23]:
# Unión de cada una de las variables al dataset inicial
df_inicial = pd.merge(df_inicial, df_data_federalfunds[['date', 'fed_funds_rate']], on='date', how='left')
df_inicial = pd.merge(df_inicial, df_data_sp500[['date', 'sp500']], on='date', how='left')
df_inicial = pd.merge(df_inicial, df_data_nasdaq[['date', 'nasdaq']], on='date', how='left')
df_inicial = pd.merge(df_inicial, df_eur_usd[['date', 'eur_usd']], on='date', how='left')
df_inicial = pd.merge(df_inicial, df_gbp_usd[['date', 'gbp_usd']], on='date', how='left')
df_inicial = pd.merge(df_inicial, df_jpy_usd[['date', 'jpy_usd']], on='date', how='left')

df_inicial
Out[23]:
date open high low close VOL_BTC VOL_USDT transactions buy_market_BTC buy_market_USDT total_supply fed_funds_rate sp500 nasdaq eur_usd gbp_usd jpy_usd
0 2017-08-17 4261.48 4485.39 4200.74 4285.08 795.150377 3.454770e+06 3427 616.248541 2.678216e+06 1.651956e+07 1.16 2430.010010 6221.910156 1.177426 1.289158 0.009091
1 2017-08-18 4285.08 4371.52 3938.77 4108.37 1199.888264 5.086958e+06 5233 972.868710 4.129123e+06 1.651956e+07 1.16 2425.550049 6216.529785 1.171550 1.286505 0.009147
2 2017-08-19 4108.37 4184.69 3850.00 4139.98 381.309763 1.549484e+06 2153 274.336042 1.118002e+06 1.651956e+07 1.16 NaN NaN NaN NaN NaN
3 2017-08-20 4120.98 4211.08 4032.62 4086.29 467.083022 1.930364e+06 2321 376.795947 1.557401e+06 1.651956e+07 1.16 NaN NaN NaN NaN NaN
4 2017-08-21 4069.13 4119.62 3911.79 4016.00 691.743060 2.797232e+06 3972 557.356107 2.255663e+06 1.651956e+07 1.16 2428.370117 6213.129883 1.175627 1.287366 0.009147
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2654 2024-11-22 98317.12 99588.01 97122.11 98892.00 46189.309243 4.555537e+09 7271311 22709.668193 2.240635e+09 1.978572e+07 4.58 5969.339844 19003.650391 1.046934 1.258479 0.006484
2655 2024-11-23 98892.00 98908.85 97136.00 97672.40 24757.843670 2.431610e+09 3839138 11176.248700 1.097539e+09 1.978626e+07 4.58 NaN NaN NaN NaN NaN
2656 2024-11-24 97672.40 98564.00 95734.77 97900.04 31200.978380 3.034172e+09 4964720 15124.121760 1.471314e+09 1.978680e+07 4.58 NaN NaN NaN NaN NaN
2657 2024-11-25 97900.05 98871.80 92600.19 93010.01 50847.450960 4.883445e+09 8289691 23486.927050 2.255517e+09 1.978734e+07 4.58 5987.370117 19054.839844 1.047987 1.259382 0.006487
2658 2024-11-26 93010.01 94973.37 90791.10 91965.16 57858.731380 5.370919e+09 10225809 29120.267650 2.702544e+09 1.978734e+07 4.58 6021.629883 19174.300781 1.044430 1.253306 0.006478

2659 rows × 17 columns

In [24]:
df_inicial.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2659 entries, 0 to 2658
Data columns (total 17 columns):
 #   Column           Non-Null Count  Dtype         
---  ------           --------------  -----         
 0   date             2659 non-null   datetime64[ns]
 1   open             2659 non-null   float64       
 2   high             2659 non-null   float64       
 3   low              2659 non-null   float64       
 4   close            2659 non-null   float64       
 5   VOL_BTC          2659 non-null   float64       
 6   VOL_USDT         2659 non-null   float64       
 7   transactions     2659 non-null   int64         
 8   buy_market_BTC   2659 non-null   float64       
 9   buy_market_USDT  2659 non-null   float64       
 10  total_supply     2659 non-null   float64       
 11  fed_funds_rate   2659 non-null   float64       
 12  sp500            1832 non-null   float64       
 13  nasdaq           1832 non-null   float64       
 14  eur_usd          1897 non-null   float64       
 15  gbp_usd          1897 non-null   float64       
 16  jpy_usd          1897 non-null   float64       
dtypes: datetime64[ns](1), float64(15), int64(1)
memory usage: 353.3 KB
In [25]:
# Se realiza una copia para trabajar con ella y poder acudir al dataframe inicial original en caso necesario
df = df_inicial.copy()

Con el dataset inicial configurado, se puede proceder al siguiente punto.

2.3. Análisis exploratorio de los datos¶

2.3.1. Descripción de los datos

El dataset inicial constará de 2659 observaciones y 17 variables. Estas variables, excepto la fecha, son todas variables numéricas, ya que se han ido incorporando datos de precios, volúmenes y ratios. De hecho, a excepción del número de transacciones ('transactions'), de tipo int, todas estas variables numéricas son de tipo float. La variable close, que hace referencia al precio de cierre de Bitcoin para cada día, será la variable target, dado que el precio que se pretende predecir es el último precio del día.

Aunque, idealmente, un modelo de red neuronal recurrente debería entrenarse con al menos el doble de observaciones para garantizar resultados más robustos, estas 2659 observaciones representan el conjunto de datos con mayor riqueza informativa obtenido durante la fase de recopilación. Si bien la falta de datos anteriores a agosto de 2017 podría limitar la capacidad del modelo para capturar tendencias de muy largo plazo, disponer de siete años de datos diarios resulta prometedor para construir un modelo capaz de predecir el precio de Bitcoin con una precisión aceptable.

A simple vista, para los valores de SP500, NASDAQ y los distintos tipos de cambio se pueden observar que existen valores nulos. Ésto se debe a que, durante los fines de semana (se puede comprobar en el dataset que dichos valores nulos aparecen en grupos de dos, coincidiendo con sábados y domingos, y algunos festivos para SP500 y NASDAQ), los mercados financieros tradicionales permanecen cerrados, a diferencia de los mercados de criptomonedas, que operan los 7 días de la semana, incluidos festivos. Durante el análisis y la preparación de los datos, será crucial decidir cómo manejar estos valores faltantes para no perder información relevante ni introducir sesgos en el modelo.

2.3.2. Análisis de las variables

close (target):

Es el precio de cierre de una sesión (día) de Bitcoin, por lo que será la variable objetivo a predecir por el modelo. Se expresa en USDT (una stablecoin cuyo valor es prácticamente parejo al dólar).

Se expondrán a continuación las métricas del precio de cierre de Bitcoin:

In [26]:
univar_num_cont(df, ['close'])
Out[26]:
Variable Valores Únicos Media Mediana Mínimo Máximo Desv. Estándar
0 close 2658 26092.58 19811.66 3189.02 98892.0 20621.99

Se puede observar que, para el periodo del que se tienen datos, el precio mínimo de cierre de Bitcoin es de 3189.02, el máximo 98892, lo cual nos indica una gran amplitud, dado que en 2017 el precio aún no había sufrido su primer bullrun que llevaría a Bitcoin a tocar por primera vez los $20000.

Distribución del precio de cierre de Bitcoin:

In [27]:
univar_graph_num_cont(df, 'close')
In [28]:
univar_graph_boxplot(df, 'close')

Del histograma del precio de cierre de Bitcoin se pueden hacer dos lecturas: la primera, que el nivel de precios de Bitcoin ha permanecido bajo durante bastante tiempo en el periodo estudiado, bastante por debajo de los 20000, y que, cuanto más alto es el precio, menos tiempo permanece en el rango de precios en cuestión (es decir, a mayor precio, menor acumulación de frecuencia de precios). Y la segunda, obviando los precios más bajos donde se acumulan la mayoría de observaciones, es que existen ciertos niveles de precios donde Bitcoin tiende a permanecer más tiempo, es decir, a acumular mayor frecuencia de observaciones; éstos son los llamados puntos de soporte o techo (según el precio se encuentre alcista o bajista), y son rangos de precio que frenan caidas o subidas. Estos se pueden observar entorno a los 20000, y en menor medida en torno a 40000 y a 60000.

Aunque se podría interpretar como un sesgo hacia la derecha de los datos, esta particularidad podría estar debida más bien a las características propias de la evolución cíclica de un mercado, por lo que no se tendrán los valores por sesgados.

La conclusión es que Bitcoin tiende a estar más tiempo en rangos de precios bajos, mientras que en precios altos la volatilidad tiende a ser mayor, reduciendo el tiempo de permanencia en esos niveles.

Por otro lado, el boxplot de la variable confirma la existencia de algunos valores outliers entre los más altos. Esto tiene sentido ya que recientemente se alcanzaron por primera vez estos valores (cercanos a 100000 dólares) en una carrera muy acelerada en las útlimas semanas.

In [29]:
bins = range(0, int(df['close'].max()) + 10000, 10000)

df_close_group = df['close'].copy()
df_close_group['close_group'] = pd.cut(df['close'], bins=bins, right=False)

group_counts = df_close_group['close_group'].value_counts().sort_index()
group_counts
Out[29]:
close
[0, 10000)         911
[10000, 20000)     426
[20000, 30000)     385
[30000, 40000)     248
[40000, 50000)     259
[50000, 60000)     162
[60000, 70000)     230
[70000, 80000)      21
[80000, 90000)       5
[90000, 100000)     12
Name: count, dtype: int64

Al agrupar los precios de cierre de Bitcoin, se confirma que tiende a estar más tiempo en precios bajos, concretamente en el rango de 0 a 10000.

date:

Para la variable de fecha, se comprobará el rango de las fechas y que no existen fechas faltantes en dicho rango.

In [30]:
print(f"Rango de fechas: {df['date'].min()} a {df['date'].max()}")
print(f"Frecuencia de observaciones: {df['date'].diff().dropna().value_counts()}")
Rango de fechas: 2017-08-17 00:00:00 a 2024-11-26 00:00:00
Frecuencia de observaciones: date
1 days    2658
Name: count, dtype: int64

El número total de días que existen entre las fechas límites es el siguiente:

In [31]:
abs(df['date'].max() - df['date'].min()).days +1
Out[31]:
2659

Relación entre la variable fecha y la variable target (serie temporal de close):

In [32]:
time_line(df)

De la representación de la evolución del precio de cierre de Bitcoin en el tiempo se puede confirmar lo que se adelantó con el histograma del precio de cierre: el precio tiende a estar más tiempo en valores bajos (desde 2017 hasta finales de 2020 estuvo por debajo de 20000, concretamente en un rango entre los 5000 y los 12000); también se pueden observar los soportes y resistencias mencionados en 20000, 40000 y 60000, siendo los precios cercanos a ellos donde Bitcoin tiende a frenar tanto en las subidas (resistencia) como en las caídas (soporte).

Por ejemplo, se puede observar el comportamiento de Bitcoin en torno a los 20000, en el primer bullrun histórico de Bitcoin, el precio alcanzó este precio, el cual ofreció resistencia y Bitcoin fue incapaz de superarlo. Más tarde, a finales de 2020, justo antes de empezar el segundo bullrun histórico, el mercado volvió a ofrecer resistencia en 20000, mientras que en 2022, en el retroceso de este bullrun, los precios en torno a 20000 actuaron como soporte y resistieron la caída.

open, high y low:

Para los precios de apertura, mayor y menor, el estudio será similar al realizado con el precio de cierre.

In [33]:
univar_num_cont(df, ['open', 'high', 'low'])
Out[33]:
Variable Valores Únicos Media Mediana Mínimo Máximo Desv. Estándar
0 open 2657 26059.54 19803.30 3188.01 98892.00 20586.69
1 high 2590 26698.73 20208.37 3276.50 99588.01 21057.72
2 low 2591 25378.98 19291.75 2817.00 97136.00 20111.94

Los datos de las métricas son muy similares entre ellos y a los de close, algo que parece lógico. Pero resulta llamativo que para high y low existan algunos valores únicos menos (para open, al igual que para close, los valores únicos coinciden con el número de observaciones, es decir, todos sus valores son distintos). Este hecho se debe a la existencia de los mencionados soportes y resistencias, valores de precios que suelen ser difíciles de romper y sobrepasar. Que haya menos valores únicos para estas dos variables significa que, en diferentes ocasiones, durante varios días seguidos, si bien el precio puede tener valores de apertura y cierre diferentes, ha sido incapaz de romper un soporte o una resistencia, marcando durante esos días el mismo valor de high o low.

Que las métricas de high sean superiores a las de low, y las de open se encuentren entre medias, nos da una idea de que los valores pueden no tener fallos, aunque no lo confirma. En caso de tener un valor mínimo de high inferior al valor mínimo de low, entonces sí que habría algún fallo en los datos que debería ser estudiado y corregido, pero, en principio, parece que los datos están acorde a lo esperado.

In [34]:
time_line(df, _vars=['open', 'high', 'low'])

Como era de esperar, los valores de apertura, máximo diario y mínimo diario son muy similares entre ellos y al precio de cierre, dibujando la misma figura los 4 en la serie temporal. Ésto nos indica que, muy probablemente, tendrán una alta correlación entre ellos.

transactions:

Para la variable de transacciones, al ser de tipo int, se calcularán métricas descriptivas y se representará la distribución de sus datos mediante un histograma.

In [35]:
univar_num_cont(df, ['transactions'])
Out[35]:
Variable Valores Únicos Media Mediana Mínimo Máximo Desv. Estándar
0 transactions 2657 1564455.45 919185.0 2153 15223589 2000917.32
In [36]:
univar_graph_num_cont(df, 'transactions', 100)

A continuación, se estudiará la variable transactions junto a la variable target:

In [37]:
bivar_graph_scatt(df, 'transactions')

De las métricas de la variable transactions se puede deducir que, aunque transactions es técnicamente una variable de tipo entero y discreta, su amplio rango de valores (mínimo de 2153 y máximo superior a 15 millones) junto con una alta cantidad de valores únicos (2657 de 2659 posibles) la hace comportarse como una variable continua en la práctica.

El histograma de transactions muestra una distribución fuertemente sesgada hacia la derecha, indicando que la mayoría de los días presentan un número bajo de transacciones, con algunos valores atípicos que representan días excepcionales de alta actividad. Este comportamiento sugiere que los días con muchas transacciones son eventos menos comunes, posiblemente asociados a momentos de alta volatilidad o a eventos específicos del mercado.

Al analizar la relación entre transactions y close, se observa que la correlación no es alta. Aunque la cantidad de transacciones tiende a influir en el precio de Bitcoin, esta relación no parece lineal ni consistente. La mayoría de las transacciones ocurren cuando el precio está en un rango moderado (alrededor de 20,000), pero también existen valores atípicos con precios altos (mayores a 80,000) y niveles elevados de transacciones, aunque son menos frecuentes.

La conclusión que se puede obtener es que, por lo general, el número de transacciones es bajo, a excepción de cuando el precio está en torno a 20000, en cuyo caso las transacciones se disparan.

Sería interesante estudiar si el número de transacciones aumenta o disminuye cuando lo hace el volumen de Bitcoin negociado en el día.

In [38]:
bivar_graph_scatt(df, 'VOL_BTC', 'transactions')

La gráfica anterior confirma que la relación entre transactions y el volumen de Bitcoin negociado diariamente es de una tendencia positiva: a mayor volumen negociado, mayor número de transacciones. Esto sugiere que la actividad de compra-venta es un motor directo del número de transacciones en la red o en el exchange. Sin embargo, hay excepciones: algunos días con un volumen negociado moderado presentan un número elevado de transacciones, lo que podría indicar un predominio de operaciones pequeñas durante esos días.

VOL_BTC y VOL_USDT:

Las siguientes variables a estudiar son las referentes a los volúmenes negociados diariamente, tanto en Bitcoin como en USDT.

In [39]:
univar_num_cont(df, ['VOL_BTC', 'VOL_USDT'])
Out[39]:
Variable Valores Únicos Media Mediana Mínimo Máximo Desv. Estándar
0 VOL_BTC 2659 6.857641e+04 4.378295e+04 228.11 7.607054e+05 8.049931e+04
1 VOL_USDT 2659 1.701214e+09 9.711511e+08 977865.73 1.746531e+10 1.997454e+09

Histogramas de los volúmenes:

In [40]:
for var in ['VOL_BTC', 'VOL_USDT']:
    univar_graph_num_cont(df, var, 100)

Boxplots para comprobar la distribución de los cuartiles y la existencia teórica de valores outliers:

In [41]:
for var in ['VOL_BTC', 'VOL_USDT']:
    univar_graph_boxplot(df, var)

Evolución temporal de los volúmenes:

In [42]:
time_line(df, _vars=['VOL_BTC'])
In [43]:
time_line(df, _vars=['VOL_USDT'])

Análisis bivariante de los volúmenes frente al precio de cierre:

In [44]:
for var in ['VOL_BTC', 'VOL_USDT']:
    bivar_graph_scatt(df, var)

Tanto para el volumen de Bitcoin como para el de USDT, la amplitud entre el valor mínimo y el máximo es considerable, así como la desviación estándar de ambos. De ambos gráficos de distribución se puede observar que también existe un fuerte sesgo, con una asimetría a la derecha que deja la mediana a la izquierda de la media. Aunque generalmente esto es indicativo de la existencia de posibles valores outliers (confirmado por los gráficos boxplots), puede ser algo normal por la naturaleza de los activos financieros, ya que ciertos períodos de alta volatilidad (como bullruns o caídas dramáticas) suelen concentrar la actividad del mercado, así como por algunos eventos extremos y poco frecuentes.

De hecho, si se comprueba la evolución en el tiempo de ambos volúmenes, se puede ver que los valores atípicos se corresponden, casi todos ellos con momentos de mucha eufória (como en el bullrun de 2021 y la lucha por resistir el soporte de los 15000 de finales de 2022). También se pueden advertir otros momentos de volumen atípico motivados por sucesos a nivel mundial externos al mundo de las criptomonedas pero que afectan a su precio, como el inicio de la pandemia COVID en marzo de 2020 que provocó una gran cantidad de liquidaciones y movimientos defensivos.

Finalmente, de la comparación del volumen de BTC con su precio se deduce que volumen tiende a ser menor a 150000 para prácticamente todos los precios de Bitcoin, a excepción de la zona de precios cercana a 20000, donde se han producido volúmenes muy superiores a lo normal. En el caso del volumen de USDT y precio de BTC se produce un hecho similar, si bien se puede vislumbrar (siempre que se obvien los outliers) una ligera correlación positiva, aunque con una pendiente muy pronunciada. El sentido de esta relación puede deberse a que, a mayor precio de Bitcoin, mayor cantidad de USDT se necesita para negociar, incluso cuando se negocian menor cantidad de BTC.

Como conclusión, se puede confirmar que el comportamiento del volumen de Bitcoin y USDT muestra patrones consistentes con la alta volatilidad y naturaleza especulativa del mercado de criptomonedas. Los eventos globales y los niveles de soporte psicológico (como, por ejemplo, los precios cercanos a 20000 USDT) juegan un papel importante en los picos de volumen, mientras que la relación positiva entre volumen de USDT y precio de Bitcoin sugiere que mayores precios requieren mayor liquidez para facilitar la negociación. Se destaca la importancia del volumen como indicador para anticipar o confirmar cambios en la tendencia del mercado.

buy_market_BTC y buy_market_USDT:

Las siguientes variables a estudiar son las referentes a las compras a mercado. Estas suponen el número de compras que se realizaron directamente a mercado, sin establecer un límite de precio, es decir, ejecutando la orden inmediatamente al mejor precio disponible en ese momento. Este tipo de órdenes son muy comunes ya que la mayoría de traders suele preferir asegurar la ejecución inmediata en lugar de arriesgarse a no completar su orden debido a fluctuaciones rápidas de precio.

Se espera encontrar una fuerte correlación entre ambas, dado que miden el mismo fenómeno en diferentes unidades. En todo caso, no será hasta el análisis bivariante que se calculará esta posible correlación.

In [45]:
univar_num_cont(df, ['buy_market_BTC', 'buy_market_USDT'])
Out[45]:
Variable Valores Únicos Media Mediana Mínimo Máximo Desv. Estándar
0 buy_market_BTC 2659 3.409902e+04 2.185593e+04 56.19 3.747756e+05 4.005416e+04
1 buy_market_USDT 2659 8.430033e+08 4.707145e+08 241363.80 8.783916e+09 9.933625e+08

Histogramas de las compras a mercado:

In [46]:
for var in ['buy_market_BTC', 'buy_market_USDT']:
    univar_graph_num_cont(df, var, 100)

Boxplots de las compras a mercado:

In [47]:
for var in ['buy_market_BTC', 'buy_market_USDT']:
    univar_graph_boxplot(df, var)

Evolución temporal de las compras a mercado:

In [48]:
time_line(df, _vars=['buy_market_BTC'])
In [49]:
time_line(df, _vars=['buy_market_USDT'])

Análisis bivariante de las compras a mercado frente al precio de cierre:

In [50]:
for var in ['buy_market_BTC', 'buy_market_USDT']:
    bivar_graph_scatt(df, var)

Tanto las métricas como los gráficos de ambas variables son prácticamente idénticos a los obtenidos para los correspondientes volúmenes, algo que tiene mucha lógica dado que las compras a mercado en BTC están contenidas dentro del volumen total negociado en BTC y las compras a mercado en USDT están contenidas dentro del volumen total negociado en USDT. Esto implica que es muy probable una alta correlación entre cada par de variables, si bien, las variables de compra a mercado pueden tener un valor agregado en modelos predictivos si capturan un subconjunto específico del volumen que puede ser relevante para ciertas dinámicas del mercado, como la presión de compra.

Las compras a mercado pueden tener un impacto más directo en la dinámica del precio debido a su naturaleza de ejecución inmediata, lo que podría justificar su inclusión en caso de que aporten valor predictivo adicional.

total_supply:

Es el suministro circulante de Bitcoin, es decir, la cantidad de Bitcoin que se encuentra disponible en el mercado para negociar. Se trata de una medida clave de la cantidad total de Bitcoin disponible en el mercado, y su crecimiento es predecible debido al límite máximo de 21 millones. La escasez de Bitcoin tiene un impacto directo en su precio: a medida que más BTC están en circulación, se vuelve más difícil minar nuevos bloques (debido a la recompensa decreciente por el halving), lo que a menudo provoca un aumento en la demanda y el precio, sobre todo si la adopción crece.

In [51]:
univar_num_cont(df, ['total_supply'])
Out[51]:
Variable Valores Únicos Media Mediana Mínimo Máximo Desv. Estándar
0 total_supply 2654 18495452.87 18676390.25 16519562.0 19787340.0 935805.23

Histograma:

In [52]:
univar_graph_num_cont(df, 'total_supply', 100)

Evolución temporal:

In [53]:
time_line(df, _vars=['total_supply'])

Boxplot:

In [54]:
univar_graph_boxplot(df, 'total_supply')

Análisis bivariante entre _totalsupply y close:

In [55]:
bivar_graph_scatt(df, 'total_supply')

Se puede comprobar que los valores mínimo y máximo del suministro total de BTC coinciden con los valores para las dos fechas más extremas de la serie temporal, lo que se deduce del carácter acumulativo de esta variable. En el histograma se puede interpretar este hecho de una manera diferente, introduciendo además el halving: se puede observar que existen 2 saltos en las frecuencias, uno en el entorno de 1.85e+07 de supply y otro pasado el 1.95e+07, de forma que las frecuencias son mayores dado que es más difícil minar nuevos Bitcoins y, por tanto, el supply aumenta más lentamente, lo que implica que permanece en el mismo nivel durante más tiempo.

La evolución temporal del supply confirma este hecho con dos momentos en los que la línea suaviza su pendiente: uno en 2020 (11/05/2020) en el entorno de 1.85e+07 de supply, y otro en 2024 (19/04/2024) pasado el 1.95e+07 de supply.

Además, el gráfico boxplot confirma que no existen datos outliers en esta variable.

Finalmente, dado que _totalsupply es una variable acumulativa que crece en el tiempo, al relacionarla con el precio se obtiene una figura muy similar a la evolución temporal del precio. Esto sugiere que no exite una relación lineal fuerte entre ambas variables, si bien podría existir una relación de otro tipo. Además, la inclusión de esta variable podría proporcionar contexto al modelo a entrenar, de forma que, si bien no ayude de forma directa a predecir el precio, pueda ayudar al modelo a entender de qué manera el aumentar el supply afecta a la velocidad a la que se producen las subidas y caídas del precio.

fed_funds_rate:

La federal funds rate es la tasa de interés de referencia de los fondos federales de Estados Unidos, empleada por los bancos cuando prestan dinero en la Reserva Federal a otras instituciones depositarias de un día para otro. Es decir, es la tasa de interés que los bancos se cobran entre sí cuando se prestan dinero. El nivel de este interés afecta a las inversiones financieras (entre ellas, inversiones en Bitcoin), ya que una política de tasas de interés bajas puede impulsar la inversión financiera, ya que los rendimientos de los bonos pueden volverse menos atractivos. Además, las decisiones de la FED (la Reserva Federal de Estados Unidos) pueden influir en la percepción de riesgo en los mercados, lo que puede llevar a movimientos bruscos en los precios.

La variable se expresa en tanto por ciento.

In [56]:
univar_num_disc(df, ['fed_funds_rate'])
Out[56]:
Variable Mínimo Perc25 Mediana Perc75 Máximo Moda Desv. Estándar
0 fed_funds_rate 0.04 0.1 1.85 4.33 5.33 5.33 1.94

Histograma de _fed_fundsrate:

In [57]:
univar_graph_num_cont(df, 'fed_funds_rate', 100)

Boxplot:

In [58]:
univar_graph_boxplot(df, 'fed_funds_rate')

Evolución en el tiempo:

In [59]:
time_line(df, _vars=['fed_funds_rate'])

Análisis bivariante entre _fed_fundsrate y close:

In [60]:
bivar_graph_scatt(df, 'fed_funds_rate')

De las métricas básicas de esta variable se pueden extraer las siguientes conclusiones:

  • Aunque nunca llega a ser 0, su valor mínimo en el periodo de referencia es de 0.04%, mientras el máximo es de 5.33%.
  • La moda coincide con el valor máximo.
  • La mediana está más cercana al mínimo que al máximo, lo que implica que los valores de esta ratio suelen ser más bajos que altos, al menos, en términos generales.

En el gráfico de distribución se puede observar la distribución irregular de la variable. Cabe destacar que, si bien el valor moda es el máximo (5.33), la barra que acumula mayor frecuencia es la primera (la que acumula los valores más cercanos a 0), lo que implica que, si bien no siempre es el mismo ratio, los valores más frecuentes están cercanos a 0. También se puede confirmar que no existen valores atípicos según el gráfico boxplot.

La evolución en el tiempo de esta variable confirma lo que se pudo ver en el histograma: existe un periodo entre el primer trimestre de 2020 y el primer trimestre de 2022 en que el interés cayó hasta casi 0 y se mantuvo durante todo el mencionado periodo (posiblemente motivado por la crisis que sobrevino con la pandemia COVID), si bien existen leves variaciones en el entorno del 0.1%. En contraste, a mediados de 2023 culmina una escalada incesante desde 2022 y el interés alcanza el 5.33, su valor máximo en el periodo del estudio, manteniéndose inamovible hasta el tercer cuatrimestre de 2024, cuando cayó por debajo del 5%.

De la comparación entre la variable y el target se puede establecer que no parece existir relación lineal alguna, aunque es importante señalar que la ausencia de correlación lineal no implica que no haya influencia. Las relaciones con tasas de interés suelen ser no lineales o condicionales (por ejemplo, tasas bajas pueden correlacionarse con mercados alcistas, pero tasas altas no necesariamente implican mercados bajistas). En cualquier caso, dado que se conoce que los movimientos de los tipos de interés influyen en las inversiones financieras, es interesante mantener esta variable para proporcionar contexto al modelo.

Por ejemplo, la caída de la tasa de interés por el COVID en 2020 coincide con una caída del precio de Bitcoin provocada por el miedo a una nueva crisis, pero pocos meses después, aún con el interés cercano a 0, BTC comenzó a recuperarse lentamente hasta tomar velocidad y marcar nuevos máximos históricos en 2021; por otro lado, el bullrun actual comenzó lentamente en 2023 y se disparó en 2024 para sufrir un retroceso y volver a dispararse de nuevo a finales de año, por lo que sería interesante que el modelo recogiera el hecho de que el bullrun comenzó lento con una tasa de interés máxima y se disparó cuando el interés bajó.

SP500 y NASDAQ:

El SP500 y el NASDAQ son dos de los índices bursátiles más importantes del mundo: SP500 es el índice más representativo de la situación real del mercado, contando la capitalización bursátil de 500 grandes empresas que poseen acciones que cotizan en las bolsas NYSE o NASDAQ, y captura aproximadamente el 80% de toda la capitalización de mercado en Estados Unidos; el NASDAQ es la segunda bolsa de valores electrónica automatizada más grande de Estados Unidos caracterizada por comprender las empresas de alta tecnología en electrónica, informática, telecomunicaciones, biotecnología, etc.

Al igual que se ha comentado con respecto a la tasa de interés de la FED, se espera que ambos mercados mantengan una relación con Bitcoin un tanto especial: sería normal encontrar una correlación positiva en momentos de crisis (pues las inversiones financieras se consideran de riesgo y los inversores tienden a refugiarse en valores seguros, como el oro) y comportarse de manera descorrelacionada en otros momentos.

In [61]:
univar_num_cont(df, ['sp500', 'nasdaq'])
Out[61]:
Variable Valores Únicos Media Mediana Mínimo Máximo Desv. Estándar
0 sp500 1828 3757.93 3822.01 2237.40 6021.63 910.90
1 nasdaq 1828 11406.84 11462.24 6192.92 19298.76 3433.19
In [62]:
for var in ['sp500', 'nasdaq']:
    univar_graph_num_cont(df, var, 100)
In [63]:
for var in ['sp500', 'nasdaq']:
    univar_graph_boxplot(df, var)
In [64]:
time_line(df, _vars=['sp500', 'nasdaq'])
In [65]:
for var in ['sp500', 'nasdaq']:
    bivar_graph_scatt(df, var)
In [66]:
bivar_graph_scatt(df, 'sp500', 'nasdaq')

De las métricas y el histograma se puede deducir que la distribución de los datos es relativamente equilibrada, como muestran una media y una mediana bastante cercanas entre sí y muy centradas, aunque ligeramente hacia la izquierda. Este posicionamiento hacia la izquierda sugiere un sesgo hacia la izquierda, lo que implica que hay más valores bajos que altos ya que, históricamente, los índices tuvieron más periodos de valores relativamente bajos que altos. Este fenómeno es común en mercados bursátiles debido a su crecimiento gradual a lo largo del tiempo. El sesgo hacia la izquierda no implica necesariamente caídas recurrentes, sino que refleja una tendencia a operar más tiempo en valores cercanos a niveles históricos que a máximos recientes.

Dado que es muy difícil que el precio cierre dos días en el mismo valor, se entiende que todos los valores serán únicos. Como sólo se poseen 1828 valores únicos para cada uno de ambos mercados, número inferior a los 2659 observaciones, se tendrá que tomar una determinación sobre cómo rellenar dichos valores nulos correspondientes, como ya se ha adelantado, a los fines de semana, cuando los índices bursátiles no continúan operando.

Los boxplots de ambas variables confirman la no existencia de valores outliers, dado que, a diferencia de Bitcoin (donde sí se encuentran outliers), estos mercados, cuando son alcistas, suben de manera más paulatina y con mucha menos volatilidad. Esto puede comprobarse en las series temporales representadas, donde se ve que ambos índices siguen la misma estructura alcista, con los mismos movimientos tanto al alza como en los retrocesos, aunque en diferentes escalas. En todo caso, ambos mercados se encuentran alcistas y estas subidas son más calmadas que en Bitcoin.

Finalmente, al realizar un análisis bivariante, los scatterplots de ambas variables con la variable target parecen indicar que existe cierta relación lineal, no extremadamente fuerte pero sí de cierta consistencia. Dado que las empresas más grandes de NASDAQ cotizan también en SP500, los incrementos y caídas del valor de sus acciones afecta en gran medida a ambos mercados, por lo que es fácil sospechar que podría existir una fuerte relación lineal entre ambos, algo que se confirma al visualizar el scatterplot resultante de relacionar ambos índices. A estas relaciones se les pondrá valor en el análisis multivariante y se tomará una decisión sobre si mantener estas variables en el dataset o eliminarlas.

Euro/Dólar, Libra/Dólar y Yen/Dólar:

Los tipos de cambio como Euro/Dólar (EURUSD), Libra/Dólar (GBPUSD) y Yen/Dólar (JPYUSD) afectan indirectamente el precio de Bitcoin a través de su impacto en la demanda de la criptomoneda en diferentes regiones. La fluctuación en el valor del dólar puede hacer que Bitcoin sea más caro o más barato en otras monedas, influir en las decisiones de inversión y actuar como un refugio de valor cuando las monedas fiduciarias pierden poder adquisitivo.

In [67]:
pairs = ['eur_usd', 'gbp_usd', 'jpy_usd']
univar_num_cont(df, pairs)
Out[67]:
Variable Valores Únicos Media Mediana Mínimo Máximo Desv. Estándar
0 eur_usd 1728 1.12 1.12 0.96 1.25 0.06
1 gbp_usd 1729 1.29 1.29 1.07 1.43 0.06
2 jpy_usd 1839 0.01 0.01 0.01 0.01 0.00
In [68]:
univar_data = []

univar_data.append(['jpy_usd', round(df['jpy_usd'].mean(),6), round(df['jpy_usd'].median(),6), round(df['jpy_usd'].min(),6), 
                    round(df['jpy_usd'].max(),6), round(np.sqrt(np.var(df['jpy_usd'])),6)])

pd.DataFrame(univar_data, columns=["Variable", "Media", "Mediana", "Mínimo", "Máximo", "Desv. Estándar"])
Out[68]:
Variable Media Mediana Mínimo Máximo Desv. Estándar
0 jpy_usd 0.008358 0.008907 0.006187 0.009739 0.001069
In [69]:
for var in pairs:
    univar_graph_num_cont(df, var, 100)
In [70]:
for var in pairs:
    univar_graph_boxplot(df, var)
In [71]:
time_line(df, _vars=['eur_usd', 'gbp_usd'])
In [72]:
time_line(df, _vars=['jpy_usd'])
In [73]:
for var in pairs:
    bivar_graph_scatt(df, var)
In [74]:
for i in range(len(pairs)):
    for j in range(i+1, len(pairs)):
        bivar_graph_scatt(df, pairs[i], pairs[j])

Las métricas de las tres variables indican lo siguiente:

  • No existen grandes amplitudes en eur_usd y gbp_usd, lo que sugiere que podrían encontrarse lateralizadas.
  • En cambio, sí existe una diferencia entre el máximo y el mínimo de jpy_usd, lo que sugiere que, o bien se encuentra en una tendencia alcista o bajista, o bien se encuentra cubriendo un lateral tan amplio que no se consideraría como lateralizada sino errática.
  • En el caso de eur_usd y gbp_usd, sus medias y medianas son muy similares en cada par, no así en jpy_usd, donde se ve una diferencia más marcada.

Si se visualizan las distribuciones de las variables, en el caso de eur_usd y gbp_usd se puede observar que tienen un cierto parecido a una distribución normal, con la media y la mediana bastante centradas, si bien, en ambos casos con una leve asimetría a la izquierda. En el caso de jpy_usd no existe tal parecido, acumulando la mayor parte de los valores por encima de 0.0085, estando la mediana (0.0089) bastante cercana al máximo.

El boxplot de gbp_usd nos informa de que esta variable es la única de las 3 que tiene una serie de valores outliers, concretamente entre los valores más pequeños.

La evolución en el tiempo de eur_usd y gbp_usd dibujan dos líneas muy parecidas, con mayor volatilidad en gbp_usd, pero con muy pocas diferencias. En cualquier caso, en el periodo estudiado no se puede considerar que exista una tendencia clara ni alcista ni bajista, confirmándose la lateralidad actual de ambos pares de divisas. Los valores mínimos de gbp_usd considerados como outliers fueron alcanzados en la segunda mitad de 2022, debido a una política fiscal expansiva del gobierno de Liz Truss que provocó una fuga masiva de capitales del Reino Unido, y coincidiendo con los mínimos valores del precio de BTC desde el bullrun de 2021 y sus picos máximos en volumen. Esto deja la impresión de que la confianza de los inversores afecta al precio de Bitcoin cuando alguna de las grandes divisas sufre fuertes caídas. Por otro lado, el hecho de que ambas gráficas sean tan parecidas indica que es muy probable que exista correlación entre ambas variables.

En el caso de jpy_usd, se encuentra en una tendencia negativa que comenzó a finales de 2021, cuando rompió la resistencia que venía respetando durante los años anteriores mientras se encontraba lateralizada. El hecho de que cayera tan pronto desde valores por encima de 0.0085 hasta valores en torno a 0.007 y 0.0065 explica por qué en la distribución aparece un espacio cuasi-vacío entre los valores más altos y los más bajos. Cabe destacar que la primera parte de la caída de jpy_usd ocurre en 2022, al igual que las caídas en eur_usd y gbp_usd, tocando un primer mínimo por las mismas fechas que lo hicieron los otros dos pares de divisas, para luego sufrir un retroceso en la caída (pequeño crecimiento a finales de 2022 y el inicio de 2023) para, finalmente continuar su descenso, a diferencia de eur_usd y gbp_usd.

Los gráficos scatterplot de los tres pares de divisas con relación al precio de Bitcoin nos indican que no existe relación lineal entre ninguna de ellas con BTC, si bien podría existir otro tipo de relación no lineal o dinámica (por ejemplo, con rezagos temporales) que podría ser relevante para el modelo. Pero, dado que se ha encontrado una posible relación lineal entre eur_usd y gbp_usd, se procede a presentar también los gráficos scatterplot de las variables entre sí, encontrando que se confirma la relación lineal muy fuerte entre las dos primeras y muy leve entre cada una de ellas con jpy_usd (dado que, si bien no existe una línea clara que se pueda dibujar en ninguno de los dos gráficos, es evidente que un mayor valor de eur_usd y gbp_usd se asocia con un mayor valor de jpy_usd). La conveniencia o no de incluir o descartar alguna de estas variables se decidirá cuando se calculen matemáticamente estas correlaciones durante el análisis multivariante, por ejemplo eliminando eur_usd o gbp_usd si ambas están muy correlacionadas y evitar así la multicolinealidad.

Análisis multivariante:

A continuación, se realizará un estudio de las correlaciones entre cada par de variables, tanto visual como matemático. Como se ha visto que pueden existir variables que tienen cierta relación pero ésta no es lineal, se añadirán al estudio de las correlaciones de Pearson (qué miden la relación lineal entre variables) el estudio de las correlaciones de Spearman y de Kendall.

  • La correlación de Spearman mide si las variables tienen una relación monótona, es decir, si una variable tiende a aumentar o disminuir de manera consistente con la otra, pero no necesariamente de manera lineal. Una relación monótona puede ser no lineal, pero sigue un patrón consistente de aumento o disminución (por ejemplo, una relación cuadrática).
  • La correlación de Kendall, por su parte, mide la relación monótona entre las dos variables, pero usa una diferente metodología de cálculo. Mide la fuerza de la asociación entre las variables basándose en la cantidad de concordancias y discordancias entre los pares de datos, considerándose dos observaciones concordantes si el orden relativo de las variables es el mismo para ambas.

Como umbral para considerar dos variables explicativas demasiado correladas se establecerá un coeficiente igual o superior a 0.9. En el caso de la variable target, se considerará poco correlada y, por tanto, poco explicativa, cualquier variable que obtenga con close un coeficiente de correlación entre 0.1 y -0.1 (muy cercanas a 0), por lo que sería un motivo bastante justificado como para eliminar dicha variable.

Correlaciones de Pearson:

In [75]:
multivar_graph_correlation_matrix(df.drop(columns=['date']))
In [76]:
df.drop(columns=['date']).corr()
Out[76]:
open high low close VOL_BTC VOL_USDT transactions buy_market_BTC buy_market_USDT total_supply fed_funds_rate sp500 nasdaq eur_usd gbp_usd jpy_usd
open 1.000000 0.999326 0.998931 0.998602 -0.050932 0.403910 0.312310 -0.055124 0.400038 0.730329 0.303723 0.916983 0.925492 -0.169105 0.147354 -0.562611
high 0.999326 1.000000 0.998691 0.999411 -0.046247 0.411396 0.316357 -0.050083 0.408123 0.727267 0.297860 0.914813 0.924352 -0.163556 0.152149 -0.557441
low 0.998931 0.998691 1.000000 0.999217 -0.057808 0.392254 0.308673 -0.061415 0.389263 0.735042 0.313374 0.920797 0.928129 -0.177801 0.138865 -0.570382
close 0.998602 0.999411 0.999217 1.000000 -0.051808 0.402791 0.312901 -0.055242 0.400108 0.730313 0.304584 0.917161 0.926011 -0.168549 0.146795 -0.562430
VOL_BTC -0.050932 -0.046247 -0.057808 -0.051808 1.000000 0.838231 0.841200 0.999515 0.839792 0.265933 0.067591 0.067349 0.037945 -0.428184 -0.476819 -0.217851
VOL_USDT 0.403910 0.411396 0.392254 0.402791 0.838231 1.000000 0.894138 0.836553 0.999435 0.494309 0.125324 0.449365 0.437706 -0.350513 -0.248227 -0.384698
transactions 0.312310 0.316357 0.308673 0.312901 0.841200 0.894138 1.000000 0.841089 0.896259 0.499894 0.272842 0.434128 0.385135 -0.486234 -0.405947 -0.474883
buy_market_BTC -0.055124 -0.050083 -0.061415 -0.055242 0.999515 0.836553 0.841089 1.000000 0.838934 0.260344 0.071001 0.064005 0.033372 -0.429640 -0.478809 -0.218185
buy_market_USDT 0.400038 0.408123 0.389263 0.400108 0.839792 0.999435 0.896259 0.838934 1.000000 0.491501 0.128230 0.446919 0.434354 -0.353478 -0.252387 -0.385840
total_supply 0.730329 0.727267 0.735042 0.730313 0.265933 0.494309 0.499894 0.260344 0.491501 1.000000 0.496232 0.891970 0.876422 -0.602098 -0.363072 -0.738556
fed_funds_rate 0.303723 0.297860 0.313374 0.304584 0.067591 0.125324 0.272842 0.071001 0.128230 0.496232 1.000000 0.477952 0.348750 -0.564029 -0.482954 -0.842434
sp500 0.916983 0.914813 0.920797 0.917161 0.067349 0.449365 0.434128 0.064005 0.446919 0.891970 0.477952 1.000000 0.981083 -0.382249 -0.059361 -0.731359
nasdaq 0.925492 0.924352 0.928129 0.926011 0.037945 0.437706 0.385135 0.033372 0.434354 0.876422 0.348750 0.981083 1.000000 -0.272990 0.030864 -0.620690
eur_usd -0.169105 -0.163556 -0.177801 -0.168549 -0.428184 -0.350513 -0.486234 -0.429640 -0.353478 -0.602098 -0.564029 -0.382249 -0.272990 1.000000 0.874794 0.728541
gbp_usd 0.147354 0.152149 0.138865 0.146795 -0.476819 -0.248227 -0.405947 -0.478809 -0.252387 -0.363072 -0.482954 -0.059361 0.030864 0.874794 1.000000 0.524519
jpy_usd -0.562611 -0.557441 -0.570382 -0.562430 -0.217851 -0.384698 -0.474883 -0.218185 -0.385840 -0.738556 -0.842434 -0.731359 -0.620690 0.728541 0.524519 1.000000

Observando el heatmap de correlaciones y el cálculo de los coeficientes de correlación de Pearson, se advierten los siguientes puntos interesantes:

  • Las variables open, high, low y la variable target (close), muestran una correlación positiva cuasi-perfecta (superior a 0.99) entre sí; esto es esperable dado que representan diferentes precios dentro de un mismo día, por lo que están estrechamente relacionadas.
  • sp500 y nasdaq presentan una correlación extremadamente alta (por encima de 0.98), lo cual es consistente con el comportamiento de dos mercados financieros que comparten gran número de empresas. Ambas variables presentan a su vez correlaciones bastante altas con _totalsupply (0.892 y 0.876 respectivamente), lo que sugiere que quizá el desempeño de los mercados bursátiles está relacionado con la oferta en circulación de Bitcoin debido posiblemente a la liquidez en el mercado, aunque también podría no existir una relación de causalidad entre ellas, siendo una simple coincidencia que las tres variables sean crecientes, sobre todo si se tiene en cuenta que los mercados SP500 y NASDAQ se encuentran en una tendencia alcista (como se vio al analizar sus evoluciones temporales) y el supply de Bitcoin es una variable acumulativa en el tiempo, por lo que aumenta en el tiempo con independencia de las tendencias alcistas o bajistas de estos índices.
  • Existe una relación positiva muy fuerte entre _eurusd y _gbpusd (0.875), confirmando la fuerte interdependencia entre las economías de la UE y Reino Unido.
  • _jpyusd tiene una correlación negativa moderada con las variables de precio de Bitcoin, en torno a -0.56, lo que sugiere una posible relación inversa entre el precio de Bitcoin y el valor relativo del Yen japonés. También presenta una correlación negativa fuerte con _fed_fundsrate (-0.842) y _totalsupply (-0.739), lo que podría indicar que un aumento en la tasa de interés de la FED o en la oferta total de Bitcoin afectaría a la depreciación del Yen frente al Dólar.
  • _VOLBTC y _buy_marketBTC están casi perfectamente correlacionadas (0.9995), indicando que ambas miden prácticamente la misma dinámica de mercado.
  • transactions y _VOLUSDT tienen una correlación alta (0.894), lo que indica que un mayor número de transacciones está acompañado de un incremento en el volumen de USDT negociado.
  • _fed_fundsrate tiene correlaciones negativas moderadas con las variables de tipos de cambio _eurusd y _gbpusd (-0.564 y -0.483 respectivamente) y bastante alta con _jpyusd (-0.842), reflejando su impacto en la valoración de las divisas ya que causa apreciación o depreciación del Dólar.
  • Finalmente, se aprecia que, sorprendentemente, _VOLBTC tiene unas correlaciones con las variables de precio de Bitcoin muy bajas (todas un poco por encima de 0.1), mientras _VOLUSDT tiene unas correlaciones medianamente altas con estas variables de precio (todas en torno a 0.75).

Correlaciones de Spearman:

In [77]:
multivar_graph_correlation_matrix(df.drop(columns=['date']), 'spearman')
In [78]:
df.drop(columns=['date']).corr(method='spearman')
Out[78]:
open high low close VOL_BTC VOL_USDT transactions buy_market_BTC buy_market_USDT total_supply fed_funds_rate sp500 nasdaq eur_usd gbp_usd jpy_usd
open 1.000000 0.999320 0.999030 0.998692 0.126852 0.753330 0.729066 0.105144 0.751067 0.818993 0.123365 0.921622 0.933239 -0.254899 0.060717 -0.508918
high 0.999320 1.000000 0.998690 0.999354 0.134570 0.757928 0.732426 0.113593 0.756121 0.816173 0.120736 0.919011 0.931359 -0.252033 0.062989 -0.506917
low 0.999030 0.998690 1.000000 0.999295 0.116099 0.747086 0.724745 0.095183 0.745266 0.823166 0.127820 0.925472 0.936840 -0.260881 0.054895 -0.511971
close 0.998692 0.999354 0.999295 1.000000 0.125614 0.752711 0.728645 0.105270 0.751245 0.819039 0.123524 0.921750 0.933977 -0.254962 0.059737 -0.508375
VOL_BTC 0.126852 0.134570 0.116099 0.125614 1.000000 0.705655 0.672823 0.997299 0.706506 0.202260 -0.199367 0.147312 0.171478 -0.231918 -0.269074 0.093797
VOL_USDT 0.753330 0.757928 0.747086 0.752711 0.705655 1.000000 0.964506 0.690400 0.999398 0.715244 0.010746 0.719845 0.727769 -0.383623 -0.175055 -0.383870
transactions 0.729066 0.732426 0.724745 0.728645 0.672823 0.964506 1.000000 0.656436 0.963143 0.771293 0.100732 0.731861 0.736457 -0.447518 -0.245412 -0.415538
buy_market_BTC 0.105144 0.113593 0.095183 0.105270 0.997299 0.690400 0.656436 1.000000 0.693344 0.181069 -0.194682 0.127987 0.151482 -0.226463 -0.268000 0.098868
buy_market_USDT 0.751067 0.756121 0.745266 0.751245 0.706506 0.999398 0.963143 0.693344 1.000000 0.712512 0.013300 0.719057 0.726497 -0.385045 -0.176763 -0.386177
total_supply 0.818993 0.816173 0.823166 0.819039 0.202260 0.715244 0.771293 0.181069 0.712512 1.000000 0.475898 0.919736 0.880240 -0.660933 -0.389872 -0.719197
fed_funds_rate 0.123365 0.120736 0.127820 0.123524 -0.199367 0.010746 0.100732 -0.194682 0.013300 0.475898 1.000000 0.296863 0.181164 -0.595917 -0.532074 -0.701242
sp500 0.921622 0.919011 0.925472 0.921750 0.147312 0.719845 0.731861 0.127987 0.719057 0.919736 0.296863 1.000000 0.980933 -0.434331 -0.091766 -0.630511
nasdaq 0.933239 0.931359 0.936840 0.933977 0.171478 0.727769 0.736457 0.151482 0.726497 0.880240 0.181164 0.980933 1.000000 -0.325291 0.000386 -0.520488
eur_usd -0.254899 -0.252033 -0.260881 -0.254962 -0.231918 -0.383623 -0.447518 -0.226463 -0.385045 -0.660933 -0.595917 -0.434331 -0.325291 1.000000 0.849135 0.699499
gbp_usd 0.060717 0.062989 0.054895 0.059737 -0.269074 -0.175055 -0.245412 -0.268000 -0.176763 -0.389872 -0.532074 -0.091766 0.000386 0.849135 1.000000 0.409807
jpy_usd -0.508918 -0.506917 -0.511971 -0.508375 0.093797 -0.383870 -0.415538 0.098868 -0.386177 -0.719197 -0.701242 -0.630511 -0.520488 0.699499 0.409807 1.000000

Al analizar las correlaciones de Spearman, que miden las posibles relaciones entre variables ya sean lineales o no, se observan las siguientes diferencias con respecto a las correlaciones lineales:

  • Las correlaciones entre _eurusd con sp500 y _totalsupply pasan de -0.22 y -0.46 (correlaciones de Pearson) a -0.43 y -0.66 (correlaciones de Spearman) respectivamente. Esto sugiere que entre _eurusd y las otras dos variables podría haber una relación más fuerte y monótona que lineal.
  • La correlación de Spearman entre _VOLUSDT y transactions es de 0.96, lo que supone superar la barrera marcada del 0.9, mientras con la correlación de Pearson no llegaba a alcanzarla (0.894). Esto sugiere que la relación entre ambas variables es mayor si no sólo se mira la relación lineal.
  • Existen otras diferencias entre los valores de las correlaciones de Pearson y de Spearman, pero no hacen cambiar el análisis anterior.

Correlaciones de Kendall:

In [79]:
multivar_graph_correlation_matrix(df.drop(columns=['date']), 'kendall')
In [80]:
df.drop(columns=['date']).corr(method='kendall')
Out[80]:
open high low close VOL_BTC VOL_USDT transactions buy_market_BTC buy_market_USDT total_supply fed_funds_rate sp500 nasdaq eur_usd gbp_usd jpy_usd
open 1.000000 0.980122 0.977722 0.970377 0.083840 0.545186 0.524059 0.070087 0.542042 0.598582 0.071434 0.754019 0.773402 -0.151143 0.043236 -0.306567
high 0.980122 1.000000 0.972273 0.981397 0.090519 0.552035 0.529332 0.077334 0.549461 0.592718 0.068767 0.749320 0.770298 -0.149335 0.044143 -0.304852
low 0.977722 0.972273 1.000000 0.979936 0.074582 0.536089 0.517664 0.061540 0.533648 0.606606 0.075081 0.761764 0.780208 -0.155780 0.039482 -0.309083
close 0.970377 0.981397 0.979936 1.000000 0.082826 0.544230 0.523535 0.070209 0.542228 0.598671 0.071359 0.754792 0.775246 -0.151749 0.042127 -0.306454
VOL_BTC 0.083840 0.090519 0.074582 0.082826 1.000000 0.538366 0.514524 0.959477 0.539279 0.159780 -0.133687 0.097198 0.106702 -0.154446 -0.184895 0.065543
VOL_USDT 0.545186 0.552035 0.536089 0.544230 0.538366 1.000000 0.843224 0.523807 0.979949 0.505818 -0.012276 0.500796 0.516655 -0.284773 -0.120487 -0.215539
transactions 0.524059 0.529332 0.517664 0.523535 0.514524 0.843224 1.000000 0.499546 0.839376 0.580768 0.038994 0.533696 0.542310 -0.325244 -0.168056 -0.239088
buy_market_BTC 0.070087 0.077334 0.061540 0.070209 0.959477 0.523807 0.499546 1.000000 0.527751 0.146539 -0.130326 0.086289 0.095273 -0.149024 -0.183126 0.068162
buy_market_USDT 0.542042 0.549461 0.533648 0.542228 0.539279 0.979949 0.839376 0.527751 1.000000 0.503286 -0.009501 0.499339 0.514453 -0.285258 -0.121302 -0.218470
total_supply 0.598582 0.592718 0.606606 0.598671 0.159780 0.505818 0.580768 0.146539 0.503286 1.000000 0.337127 0.780122 0.730083 -0.446921 -0.228691 -0.516758
fed_funds_rate 0.071434 0.068767 0.075081 0.071359 -0.133687 -0.012276 0.038994 -0.130326 -0.009501 0.337127 1.000000 0.201896 0.128992 -0.414185 -0.378449 -0.516731
sp500 0.754019 0.749320 0.761764 0.754792 0.097198 0.500796 0.533696 0.086289 0.499339 0.780122 0.201896 1.000000 0.896358 -0.279059 -0.053879 -0.405230
nasdaq 0.773402 0.770298 0.780208 0.775246 0.106702 0.516655 0.542310 0.095273 0.514453 0.730083 0.128992 0.896358 1.000000 -0.228552 -0.008476 -0.332174
eur_usd -0.151143 -0.149335 -0.155780 -0.151749 -0.154446 -0.284773 -0.325244 -0.149024 -0.285258 -0.446921 -0.414185 -0.279059 -0.228552 1.000000 0.664521 0.489179
gbp_usd 0.043236 0.044143 0.039482 0.042127 -0.184895 -0.120487 -0.168056 -0.183126 -0.121302 -0.228691 -0.378449 -0.053879 -0.008476 0.664521 1.000000 0.270715
jpy_usd -0.306567 -0.304852 -0.309083 -0.306454 0.065543 -0.215539 -0.239088 0.068162 -0.218470 -0.516758 -0.516731 -0.405230 -0.332174 0.489179 0.270715 1.000000

Al analizar las correlaciones de Kendall, que miden las relaciones monótonas de manera más robusta frente a valores extremos que Spearman, se observan las siguientes diferencias:

  • Con respecto a las correlaciones de _totalsupply y _eurusd, la correlación de Kendall se alinea más con la de Pearson (-0.45 y -0.46 respectivamente) que con la de Spearman (-0.66), lo que podría indicar que la relación puede no ser consistentemente monótona y que la alta correlación de Spearman podría ser influenciada por no linealidades más marcadas.
  • En el caso de _totalsupply y sp500, la correlación de Kendall (0.78) muestra un cierto aumento frente a las correlaciones de Spearman (0.74) y Pearson (0.68), lo que podría significar que, aunque la relación sea monótona, las variaciones en magnitud (que afectan a la correlación de Pearson) son menos relevantes.
  • En las relaciones entre sp500 y nasdaq con _eurusd, en ambos casos la correlación de Kendall (-0.28 para sp500 y _eurusd, y -0.23 para nasdaq y _eurusd) suaviza la magnitud respecto a la de Spearman (-0.43 y -0.34 respectivamente), acercándose en ambos casos a la de Pearson (-0.22 y -0.17 respectivamente), sugiriendo que ambas relaciones son más lineales de que lo indica el coeficiente de Spearman, posiblemente por una menor influencia de los valores extremos de lo que se estima con este indicador.
  • Entre _VOLUSDT y transactions existen unas correlaciones de Pearson y de Spearman de 0.894 y 0.96 respectivamente, mientras que el coeficiente de Kendall rebaja la relación hasta 0.84, sugiriendo que la relación entre ambas variables es principalmente lineal y no existe una fuerte influencia de valores extremos.
  • El resto de diferencias no suponen cambios en los análisis anteriores.

Como conclusión al estudio de las correlaciones, tanto para relaciones lineales como para relaciones monótonas, se puede afirmar lo siguiente:

  • open, close, high y low: Dado que las variables de precio tienen entre ellas una alta correlación (por encima de 0.99), serían candidatas (a excepción de close por ser la variable target) a ser eliminadas del dataset para evitar la multicolinealidad. Sin embargo, en el contexto de series temporales y análisis de activos financieros, estas variables no son redundantes de forma directa, pues no solo contienen valores históricos de precio, sino también información secuencial que puede ser crucial para predecir el comportamiento del precio de cierre. Por lo tanto, y a pesar de la correlación estadística, en principio se decide mantener todas las variables de precio por aportar contexto valioso para el comportamiento del mercado, si bien ninguno de los modelos seleccionados para realizar la comparativa debería ser lineal, puesto que estarían afectados de inicio por esta multicolinealidad. En caso de necesitar entrenar un modelo lineal, o si los modelos no lineales entrenados se ven afectados por la redundancia, se debería considerar la posibilidad de transformar estas variables, por ejemplo sustituyéndolas por indicadores derivados como el rango (restando high y low) y la diferencia entre los precios de apertura y cierre.
  • sp500 y nasdaq: ambas tienen correlaciones lineales superiores a 0.9 con close, y, al mismo tiempo, tienen una correlación lineal también muy alta entre ellas (0.98). Esto implica que el comportamiento de ambos índices está alineado con el precio de Bitcoin al mismo tiempo que suponen una fuerte redundancia al ser índices que se mueven de manera muy sincronizada. Por lo tanto, la decisión será la de eliminar una de las dos variables (nasdaq) y mantener la otra (sp500). Se mantendrá la variable que representa el valor del SP500 al ser éste un índice más representativo del mercado global (incluye empresas de diferentes sectores, no sólo del tecnológico como NASDAQ), por lo que podría tener una correlación más diversificada con factores que afectan al mercado global, mientras nasdaq podría ser más específico y redundante.
  • _eurusd y _gbpusd: Aunque existe una correlación positiva muy fuerte (0.875) entre ambas variables, no alcanza el umbral que se emplea para decidir la eliminación de una variable (0.9), por lo que se mantendrán ambas en el dataset.
  • _VOLBTC y _buy_marketBTC / _VOLUSDT y _buy_marketUSDT: En ambas relaciones, el coeficiente de Pearson es superior a 0.99, lo que indica que las correlaciones son casi perfectas. Dado que las variables de compras a mercado están contenidas dentro de las variables de volumen, es lógico que estén tan correlacionadas. Se decide que la estrategia a seguir con estas variables será transformarlas en variables que representen el porcentaje que las órdenes de compra a mercado suponen con respecto a los volúmenes totales, de forma que se aporta información sobre cómo se comporta el mercado sin suponer una redundancia (como sí supondría el mantener estas variables sin modificar), como el predominio de las órdenes a mercado en momentos de alta o baja volatilidad. La introducción de estos porcentajes de órdenes a mercado puede ser un indicador valioso sobre las tendencias agresivas o conservadoras de los traders en determinados momentos.
  • _VOLUSDT y transactions: Los altos coeficientes alcanzados por estas dos variables en sus relaciones hacen indicar que existe una relación muy fuerte entre ambas; dado que los coeficientes de Pearson (0.89) y Kendall (0.84) no alcanzan el umbral de 0.9, se decide no eliminar ninguna de estas variables, a pesar de que el coeficiente de Spearman sí lo sobrepasa (0.96). Aunque puedan provocar cierta redundancia y, por tanto, problemas de colinealidad, ambas variables parecen aportar algo de información única. Concretamente, aunque ambas variables miden el mismo fenómeno (las transacciones en valor monetario y en número de operaciones respectivamente), un modelo complejo podría ser capaz de captar relaciones complejas entre la cantidad y el valor de las transacciones, pudiendo distinguir entre un volumen alto debido a muchas pequeñas transacciones o a pocas grandes transacciones. Por lo tanto, la decisión es la de mantener, en principio, ambas variables.
  • Variables poco correladas con close: Aplicando el umbral de 0.1 para reconocer una variable como poco correlada con la variable target, las únicas que parecen tener un coeficiente de correlación lineal entre 0.1 y -0.1 con close son _VOLBTC y _buy_marketBTC (-0.05 ambas), si bien, al calcular el coeficiente de correlación de Spearman, ambas superan, por poco, dicho umbral (0.125 y 0.105 respectivamente), por lo que se las considera lo suficientemente explicativas como para no eliminarlas. Estas variables podrían tener una relación no lineal con close suficiente como para aportar información valiosa al modelo.
  • Resto de correlaciones: El resto de correlaciones no son lo suficientemente significativas como para considerar dos variables explicativas correlacionadas.

2.4. Calidad de los datos¶

2.4.1. Valores nulos

Anteriormente se ha comprobado la existencia de valores nulos en algunas variables, concretamente para sp500 y nasdaq por un lado, y para _eurusd, _gbpusd y _jpyusd por otro. Para las variables sobre índices bursátiles existen 827 valores nulos y para las variables sobre pares de divisas existen 762 valores nulos.

In [81]:
df.isnull().sum()
Out[81]:
date                 0
open                 0
high                 0
low                  0
close                0
VOL_BTC              0
VOL_USDT             0
transactions         0
buy_market_BTC       0
buy_market_USDT      0
total_supply         0
fed_funds_rate       0
sp500              827
nasdaq             827
eur_usd            762
gbp_usd            762
jpy_usd            762
dtype: int64

En el caso de las variables sobre pares de divisas, aunque FOREX opera los siete días de la semana, muchos proveedores de datos no actualizan sus series temporales de divisas durante el fin de semana, normalmente debido a que suelen basarse en los mercados bursátiles tradicionales, como SP500 y NASDAQ, que sí permanecen cerrados durante el fin de semana. Este parece ser el caso de la biblioteca de Yahoo Finance, de la que se han obtenido los datos de los pares de divisas.

En el caso de las variables sobre índices bursátiles, como se ha comentado, los mercados bursátiles tradicionales cierran los fines de semana, por lo que también faltan dichos datos en el dataset. La diferencia que existe entre ambos (mayor número de valores nulos en las variables sobre índices bursátiles que en las variables sobre pares de divisas) se debe a que, además de los fines de semana, los mercados tradicionales americanos permanecen cerrados en ciertas festividades nacionales, como el Día del Trabajo, el Día de Acción de Gracias o el Día de Navidad, entre otros. Durante estos días festivos en Estados Unidos, FOREX continúa operando y los proveedores de datos sí ofrecen dichas cotizaciones.

Si se fuerza a mostrar aquellas observaciones que cumplen simultáneamente que sp500 y nasdaq son nulos, se puede comprobar que se obtienen las 827 observaciones que para ambos son nulas, lo que significa que los 827 valores nulos de ambos son exactamente en las mismas fechas.

Fechas para las que sp500 y nasdaq son nulos:

In [82]:
df[df['sp500'].isnull() & df['nasdaq'].isnull()][['date', 'sp500', 'nasdaq']]
Out[82]:
date sp500 nasdaq
2 2017-08-19 NaN NaN
3 2017-08-20 NaN NaN
9 2017-08-26 NaN NaN
10 2017-08-27 NaN NaN
16 2017-09-02 NaN NaN
... ... ... ...
2642 2024-11-10 NaN NaN
2648 2024-11-16 NaN NaN
2649 2024-11-17 NaN NaN
2655 2024-11-23 NaN NaN
2656 2024-11-24 NaN NaN

827 rows × 3 columns

Lo mismo ocurre con las tres variables sobre pares de divisas: cada una tiene 762 valores nulos, y si se fuerza a que se muestren sólo las observaciones que cumplen que las tres son nulas, se comprueba que se obtienen 762 observaciones. Por tanto, al igual que pasaba con sp500 y nasdaq, todos los valores nulos aparecen en las mismas fechas para estas tres variables.

Fechas para las que _eurusd, _gbpusd y _jpyusd son nulos:

In [83]:
df[df['eur_usd'].isnull() & df['gbp_usd'].isnull() & df['jpy_usd'].isnull()][['date', 'eur_usd', 'gbp_usd', 'jpy_usd']]
Out[83]:
date eur_usd gbp_usd jpy_usd
2 2017-08-19 NaN NaN NaN
3 2017-08-20 NaN NaN NaN
9 2017-08-26 NaN NaN NaN
10 2017-08-27 NaN NaN NaN
16 2017-09-02 NaN NaN NaN
... ... ... ... ...
2642 2024-11-10 NaN NaN NaN
2648 2024-11-16 NaN NaN NaN
2649 2024-11-17 NaN NaN NaN
2655 2024-11-23 NaN NaN NaN
2656 2024-11-24 NaN NaN NaN

762 rows × 4 columns

Finalmente, si se observan las fechas en que se diferencian las variables sobre índices bursátiles (pues son nulas) y las variables sobre pares de divisas (pues tienen datos), se puede comprobar fácilmente que son fechas señaladas como festividades en los Estados Unidos. Por ejemplo, el 02/09/2024 fue el Día del Trabajo (Labor Day) o el 25/12/2017 fue Navidad (Christmas Day), confirmándose que estas diferencias se deben a ciertas festividades.

In [84]:
df[df['sp500'].isnull() & df['nasdaq'].isnull() \
   & pd.notna(df['eur_usd']) & pd.notna(df['gbp_usd']) & pd.notna(df['jpy_usd'])] \
        [['date', 'sp500', 'nasdaq', 'eur_usd', 'gbp_usd', 'jpy_usd']]
Out[84]:
date sp500 nasdaq eur_usd gbp_usd jpy_usd
18 2017-09-04 NaN NaN 1.188284 1.296512 0.009108
98 2017-11-23 NaN NaN 1.181474 1.331682 0.008984
130 2017-12-25 NaN NaN 1.185607 1.336505 0.008826
137 2018-01-01 NaN NaN 1.200495 1.351607 0.008876
151 2018-01-15 NaN NaN 1.219230 1.373306 0.009001
... ... ... ... ... ... ...
2416 2024-03-29 NaN NaN 1.079447 1.262626 0.006603
2475 2024-05-27 NaN NaN 1.084763 1.273610 0.006375
2498 2024-06-19 NaN NaN 1.074229 1.270745 0.006334
2513 2024-07-04 NaN NaN 1.079331 1.274778 0.006187
2573 2024-09-02 NaN NaN 1.104484 1.312853 0.006831

67 rows × 6 columns

Teniendo en cuenta que para sp500 y nasdaq los fines de semana y festivos se paraliza la cotización, el último precio de cierre anterior, es decir, los viernes o víspera de festivo, es el precio de cierre que se mantiene para estos días. Como no se desea perder la información que estas observaciones poseen para el resto de variables, se descarta la eliminación de las observaciones con valores nulos, y la decisión para rellenar los valores faltantes en estas dos variables será el mantener el último valor de cierre inmediatamente anterior a cada valor faltante. A pesar de que podría introducir inconsistencias en los datos, el perjucio por esto se considera muy inferior al beneficio de mantener dichas observaciones en el dataset.

In [85]:
df[['sp500', 'nasdaq']] = impute_forward_fill(df, ['sp500', 'nasdaq'])

Para el caso de _eurusd, _gbpusd y _jpyusd, dado que las cotización no se paralizan en fines de semana, no tiene sentido imputar el valor último conocido anterior, siendo una opción mucho más acertada la interpolación lineal para obtener unas aproximaciones que no supongan la "congelación" del precio durante el fin de semana. Aunque no sean valores reales sino unas aproximaciones, el sesgo introducido debería ser menor que mediante el método de imputación empleado con 'sp500' y 'nasdaq'.

In [86]:
df[['eur_usd', 'gbp_usd', 'jpy_usd']] = impute_interpolate(df, ['eur_usd', 'gbp_usd', 'jpy_usd'])
In [87]:
df[['date', 'sp500', 'nasdaq', 'eur_usd', 'gbp_usd', 'jpy_usd']]
Out[87]:
date sp500 nasdaq eur_usd gbp_usd jpy_usd
0 2017-08-17 2430.010010 6221.910156 1.177426 1.289158 0.009091
1 2017-08-18 2425.550049 6216.529785 1.171550 1.286505 0.009147
2 2017-08-19 2425.550049 6216.529785 1.172909 1.286792 0.009147
3 2017-08-20 2425.550049 6216.529785 1.174268 1.287079 0.009147
4 2017-08-21 2428.370117 6213.129883 1.175627 1.287366 0.009147
... ... ... ... ... ... ...
2654 2024-11-22 5969.339844 19003.650391 1.046934 1.258479 0.006484
2655 2024-11-23 5969.339844 19003.650391 1.047285 1.258780 0.006485
2656 2024-11-24 5969.339844 19003.650391 1.047636 1.259081 0.006486
2657 2024-11-25 5987.370117 19054.839844 1.047987 1.259382 0.006487
2658 2024-11-26 6021.629883 19174.300781 1.044430 1.253306 0.006478

2659 rows × 6 columns

Finalmente, se comprueba que no existen más valores nulos en el dataset.

In [88]:
df.isna().sum()
Out[88]:
date               0
open               0
high               0
low                0
close              0
VOL_BTC            0
VOL_USDT           0
transactions       0
buy_market_BTC     0
buy_market_USDT    0
total_supply       0
fed_funds_rate     0
sp500              0
nasdaq             0
eur_usd            0
gbp_usd            0
jpy_usd            0
dtype: int64

2.4.2. Registros duplicados

El siguiente paso en la comprobación de la calidad de los datos es asegurarse de que no existen registros duplicados. Para ello, se procede a la eliminación de cualquier observación que aparezca duplicada mediante la función _dropduplicates(), que eliminará tantas observaciones repetidas como existan hasta asegurarse de que sólo permanece una única de ellas en el dataset. Cuando se estudió la variable date se comprobó que el número de días entre las fechas límite era de 2659, lo que coincide con el número total de observaciones, por lo que no deberían de existir ni días faltantes ni observaciones duplicadas.

In [89]:
print(f"Número de registros actual: {max(df.count())}")
df = df.drop_duplicates()
print(f"Número de registros tras eliminación de duplicados: {max(df.count())}")
Número de registros actual: 2659
Número de registros tras eliminación de duplicados: 2659

Como se sospechaba, no se ha eliminado ninguna observación, por lo que el dataset se encontraba libre de datos duplicados.

2.5. Identificación de outliers¶

Durante el análisis de las variables, las gráficas boxplot han avisado de valores outliers en las variables close, _VOLBTC, _VOLUSDT, _buy_marketBTC, _buy_marketUSDT y _gbpusd.

En una serie temporal, cada observación está intrínsecamente relacionada con las anteriores y las siguientes, por lo que la eliminación de datos intermedios puede generar lagunas que dificulten al modelo el entendimiento correcto de los datos. En el contexto concreto de los mercados financieros, un outlier no indica necesariamente un error, sino que puede reflejar eventos significativos como alta volatilidad, noticias relevantes o cambios abruptos en la oferta y/o demanda. Por lo tanto, estos outliers pueden ser cruciales para entender el comportamiento del mercado y pueden aportar valor en la fase de modelado. Además, el principal modelo que se pretende entrenar es una red neuronal recurrente, que suele ser robusta ante la presencia de estos valores.

Por lo tanto, se decide optar por una estrategia diferente a la eliminación de las observaciones con datos outliers, como es la normalización por z-score. De esta manera se evita la eliminación pero se atenúa su impacto. Además, se aplicará dentro de ventanas de tiempo mensual para que el modelo pueda aprender mejor las dinámicas a lo largo del tiempo, enfocándose en patrones a corto plazo y reduciendo la influencia de cambios bruscos.

Esta normalización se llevará a cabo en la fase de transformación de variables, puesto que es necesario realizar las transformaciones antes de normalizar.

3. Preparación de los datos¶

3.1. Eliminación de variables poco representativas¶

Durante el análisis de las variables se han encontrado varias con altos coeficientes de correlación (lineal o monótona), siendo la correlación entre variables explicativas una de las principales causas para descartar alguna de ellas. En este caso concreto, se ha justificado la no eliminación de varias de ellas (como las variables de precio de Bitcoin, muy correladas entre sí, y las variables de volumen y compras a mercado, pues todas ellas serán transformadas para eliminar la multicolinealidad y, al mismo, mantener la riqueza informativa del dataset.

La única variable que se eliminará en esta fase será nasdaq, pues su correlación con sp500 es de 0.98 (Coeficiente de Pearson), y ya se justificó anteriormente la permanencia de sp500 sobre ésta.

In [90]:
df.drop(columns=['nasdaq'], inplace=True)
df
Out[90]:
date open high low close VOL_BTC VOL_USDT transactions buy_market_BTC buy_market_USDT total_supply fed_funds_rate sp500 eur_usd gbp_usd jpy_usd
0 2017-08-17 4261.48 4485.39 4200.74 4285.08 795.150377 3.454770e+06 3427 616.248541 2.678216e+06 1.651956e+07 1.16 2430.010010 1.177426 1.289158 0.009091
1 2017-08-18 4285.08 4371.52 3938.77 4108.37 1199.888264 5.086958e+06 5233 972.868710 4.129123e+06 1.651956e+07 1.16 2425.550049 1.171550 1.286505 0.009147
2 2017-08-19 4108.37 4184.69 3850.00 4139.98 381.309763 1.549484e+06 2153 274.336042 1.118002e+06 1.651956e+07 1.16 2425.550049 1.172909 1.286792 0.009147
3 2017-08-20 4120.98 4211.08 4032.62 4086.29 467.083022 1.930364e+06 2321 376.795947 1.557401e+06 1.651956e+07 1.16 2425.550049 1.174268 1.287079 0.009147
4 2017-08-21 4069.13 4119.62 3911.79 4016.00 691.743060 2.797232e+06 3972 557.356107 2.255663e+06 1.651956e+07 1.16 2428.370117 1.175627 1.287366 0.009147
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2654 2024-11-22 98317.12 99588.01 97122.11 98892.00 46189.309243 4.555537e+09 7271311 22709.668193 2.240635e+09 1.978572e+07 4.58 5969.339844 1.046934 1.258479 0.006484
2655 2024-11-23 98892.00 98908.85 97136.00 97672.40 24757.843670 2.431610e+09 3839138 11176.248700 1.097539e+09 1.978626e+07 4.58 5969.339844 1.047285 1.258780 0.006485
2656 2024-11-24 97672.40 98564.00 95734.77 97900.04 31200.978380 3.034172e+09 4964720 15124.121760 1.471314e+09 1.978680e+07 4.58 5969.339844 1.047636 1.259081 0.006486
2657 2024-11-25 97900.05 98871.80 92600.19 93010.01 50847.450960 4.883445e+09 8289691 23486.927050 2.255517e+09 1.978734e+07 4.58 5987.370117 1.047987 1.259382 0.006487
2658 2024-11-26 93010.01 94973.37 90791.10 91965.16 57858.731380 5.370919e+09 10225809 29120.267650 2.702544e+09 1.978734e+07 4.58 6021.629883 1.044430 1.253306 0.006478

2659 rows × 16 columns

3.2. Limpieza de datos¶

La limpieza de datos consiste básicamente en la eliminación de los valores nulos, bien mediante la eliminación de las observaciones que los contienen, bien mediante la imputación de un valor representativo. En este caso, se ha aprovechado la fase de estudio de valores nulos para ejecutar la solución sobre la marcha, por lo que esta fase ya está realizada.

Se comprueba cómo el dataset ya contiene 2659 valores para cada variable, de un total de 2659 observaciones.

In [91]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2659 entries, 0 to 2658
Data columns (total 16 columns):
 #   Column           Non-Null Count  Dtype         
---  ------           --------------  -----         
 0   date             2659 non-null   datetime64[ns]
 1   open             2659 non-null   float64       
 2   high             2659 non-null   float64       
 3   low              2659 non-null   float64       
 4   close            2659 non-null   float64       
 5   VOL_BTC          2659 non-null   float64       
 6   VOL_USDT         2659 non-null   float64       
 7   transactions     2659 non-null   int64         
 8   buy_market_BTC   2659 non-null   float64       
 9   buy_market_USDT  2659 non-null   float64       
 10  total_supply     2659 non-null   float64       
 11  fed_funds_rate   2659 non-null   float64       
 12  sp500            2659 non-null   float64       
 13  eur_usd          2659 non-null   float64       
 14  gbp_usd          2659 non-null   float64       
 15  jpy_usd          2659 non-null   float64       
dtypes: datetime64[ns](1), float64(14), int64(1)
memory usage: 332.5 KB

3.3. Creación y transformación de nuevas variables¶

El análisis de variables ha puesto de manifiesto una relación lineal cuasi-perfecta entre las variables de precio de Bitcoin (open, high y low, obviando close por ser la variable target). Si bien, ante tal hecho, lo natural sería eliminar dos de las tres variables de precio, se ha establecido que dichas variables no son redundantes de forma directa, pues contienen información importante acerca del comportamiento del precio a lo largo de cada día (en qué precio abrió, qué precio máximo alcanzó, y en qué precio mínimo se frenó), por lo que su eliminación se descartó. Ahora bien, tampoco sería una buena práctica mantenerlas sin modificación alguna, por lo que se han decidido dos transformaciones simples que mantendrán la información aportada por las 3 variables de forma que se podrán eliminar y se evitará la posible multicolinealidad en algunos modelos.

Por un lado, se creará la variable range como diferencia entre high y low, y representará la amplitud de precios alcanzada cada día. Por otro lado, se creará la variable _delta_openclose como diferencia entre el precio de apertura y de cierre, representando la amplitud entre el primer y el último precio de cada día.

Además, antes de eliminar las variables de precio, se introducirá una nueva variable que representará el porcentaje de crecimiento o decrecimiento, según el caso, del precio de Bitcoin con respecto a su precio de apertura. Esto introduce la escala relativa del comporamiento del precio a lo largo del día, facilitando al modelo que pueda entender la magnitud del cambio del precio respecto de su nivel inicial. Al estar basada en las mismas variables que _delta_openclose, podría pensarse que se puede introducir una redundancia, aunque ésta no debería ser muy problemática ya que representa la información desde una perspectiva diferente. En cualquier caso, una vez introducidos los cambios, se estudiará su correlación para decidir si se mantiene o se elimina.

In [92]:
df['range'] = df['high'] - df['low']
df['delta_open_close'] = df['open'] - df['close']
df['percent_variation'] = (df['close'] - df['open']) / df['open']

df.drop(columns=['open', 'high', 'low'], inplace=True)
df
Out[92]:
date close VOL_BTC VOL_USDT transactions buy_market_BTC buy_market_USDT total_supply fed_funds_rate sp500 eur_usd gbp_usd jpy_usd range delta_open_close percent_variation
0 2017-08-17 4285.08 795.150377 3.454770e+06 3427 616.248541 2.678216e+06 1.651956e+07 1.16 2430.010010 1.177426 1.289158 0.009091 284.65 -23.60 0.005538
1 2017-08-18 4108.37 1199.888264 5.086958e+06 5233 972.868710 4.129123e+06 1.651956e+07 1.16 2425.550049 1.171550 1.286505 0.009147 432.75 176.71 -0.041238
2 2017-08-19 4139.98 381.309763 1.549484e+06 2153 274.336042 1.118002e+06 1.651956e+07 1.16 2425.550049 1.172909 1.286792 0.009147 334.69 -31.61 0.007694
3 2017-08-20 4086.29 467.083022 1.930364e+06 2321 376.795947 1.557401e+06 1.651956e+07 1.16 2425.550049 1.174268 1.287079 0.009147 178.46 34.69 -0.008418
4 2017-08-21 4016.00 691.743060 2.797232e+06 3972 557.356107 2.255663e+06 1.651956e+07 1.16 2428.370117 1.175627 1.287366 0.009147 207.83 53.13 -0.013057
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2654 2024-11-22 98892.00 46189.309243 4.555537e+09 7271311 22709.668193 2.240635e+09 1.978572e+07 4.58 5969.339844 1.046934 1.258479 0.006484 2465.90 -574.88 0.005847
2655 2024-11-23 97672.40 24757.843670 2.431610e+09 3839138 11176.248700 1.097539e+09 1.978626e+07 4.58 5969.339844 1.047285 1.258780 0.006485 1772.85 1219.60 -0.012333
2656 2024-11-24 97900.04 31200.978380 3.034172e+09 4964720 15124.121760 1.471314e+09 1.978680e+07 4.58 5969.339844 1.047636 1.259081 0.006486 2829.23 -227.64 0.002331
2657 2024-11-25 93010.01 50847.450960 4.883445e+09 8289691 23486.927050 2.255517e+09 1.978734e+07 4.58 5987.370117 1.047987 1.259382 0.006487 6271.61 4890.04 -0.049949
2658 2024-11-26 91965.16 57858.731380 5.370919e+09 10225809 29120.267650 2.702544e+09 1.978734e+07 4.58 6021.629883 1.044430 1.253306 0.006478 4182.27 1044.85 -0.011234

2659 rows × 16 columns

In [93]:
df[['close', 'sp500', 'range', 'delta_open_close', 'percent_variation']].corr()
Out[93]:
close sp500 range delta_open_close percent_variation
close 1.000000 0.916830 0.670597 -0.058786 0.019800
sp500 0.916830 1.000000 0.525607 -0.033450 -0.001138
range 0.670597 0.525607 1.000000 0.031196 -0.035328
delta_open_close -0.058786 -0.033450 0.031196 1.000000 -0.761566
percent_variation 0.019800 -0.001138 -0.035328 -0.761566 1.000000

Al realizar el estudio de correlaciones entre las nuevas variables y la variable target se decide estudiar también su relación con la variable sp500, ya que las variables en las que se basan las nuevas estaban altamente correlacionadas con sp500 y es interesante ver que esta correlación ha desaparecido. Por otro lado, la correlación entre _delta_openclose y _percentvariation es moderadamente elevada y negativa (-0.76), por lo que no existirán problemas graves de multicolinealidad entre ellas.

La baja correlación entre _delta_openclose y _percentvariation con close no significa necesariamente que no sean variables válidas, pues podrían aportar información complementaria al modelo.

Por otro lado, también durante el estudio de correlaciones se detectó una muy fuerte relación lineal entre las variables de volumen y las variables de compras a mercado, por lo que también por aquí podrían surgir problemas de multicolinealidad. Para evitarla y no perder información relevante sobre las compras a mercado o el volumen total, en lugar de eliminar una de ellas, se transformará la variable de compras de mercado como porcentaje sobre el volumen total. De igual modo, se estudiará posteriormente las correlaciones entre las variables de volumen y los porcentajes obtenidos para comprobar que se ha reducido el problema de la multicolinealidad.

In [94]:
df['percent_buy_market_BTC'] = df['buy_market_BTC'] / df['VOL_BTC']
df['percent_buy_market_USDT'] = df['buy_market_USDT'] / df['VOL_USDT']

df.drop(columns=['buy_market_BTC', 'buy_market_USDT'], inplace=True)
df
Out[94]:
date close VOL_BTC VOL_USDT transactions total_supply fed_funds_rate sp500 eur_usd gbp_usd jpy_usd range delta_open_close percent_variation percent_buy_market_BTC percent_buy_market_USDT
0 2017-08-17 4285.08 795.150377 3.454770e+06 3427 1.651956e+07 1.16 2430.010010 1.177426 1.289158 0.009091 284.65 -23.60 0.005538 0.775009 0.775223
1 2017-08-18 4108.37 1199.888264 5.086958e+06 5233 1.651956e+07 1.16 2425.550049 1.171550 1.286505 0.009147 432.75 176.71 -0.041238 0.810799 0.811708
2 2017-08-19 4139.98 381.309763 1.549484e+06 2153 1.651956e+07 1.16 2425.550049 1.172909 1.286792 0.009147 334.69 -31.61 0.007694 0.719457 0.721532
3 2017-08-20 4086.29 467.083022 1.930364e+06 2321 1.651956e+07 1.16 2425.550049 1.174268 1.287079 0.009147 178.46 34.69 -0.008418 0.806700 0.806791
4 2017-08-21 4016.00 691.743060 2.797232e+06 3972 1.651956e+07 1.16 2428.370117 1.175627 1.287366 0.009147 207.83 53.13 -0.013057 0.805727 0.806391
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2654 2024-11-22 98892.00 46189.309243 4.555537e+09 7271311 1.978572e+07 4.58 5969.339844 1.046934 1.258479 0.006484 2465.90 -574.88 0.005847 0.491665 0.491849
2655 2024-11-23 97672.40 24757.843670 2.431610e+09 3839138 1.978626e+07 4.58 5969.339844 1.047285 1.258780 0.006485 1772.85 1219.60 -0.012333 0.451423 0.451363
2656 2024-11-24 97900.04 31200.978380 3.034172e+09 4964720 1.978680e+07 4.58 5969.339844 1.047636 1.259081 0.006486 2829.23 -227.64 0.002331 0.484732 0.484914
2657 2024-11-25 93010.01 50847.450960 4.883445e+09 8289691 1.978734e+07 4.58 5987.370117 1.047987 1.259382 0.006487 6271.61 4890.04 -0.049949 0.461910 0.461870
2658 2024-11-26 91965.16 57858.731380 5.370919e+09 10225809 1.978734e+07 4.58 6021.629883 1.044430 1.253306 0.006478 4182.27 1044.85 -0.011234 0.503299 0.503181

2659 rows × 16 columns

In [95]:
df[['close', 'VOL_BTC', 'VOL_USDT', 'percent_buy_market_BTC', 'percent_buy_market_USDT']].corr()
Out[95]:
close VOL_BTC VOL_USDT percent_buy_market_BTC percent_buy_market_USDT
close 1.000000 -0.051808 0.402791 -0.108420 -0.109291
VOL_BTC -0.051808 1.000000 0.838231 -0.006600 -0.007067
VOL_USDT 0.402791 0.838231 1.000000 -0.052582 -0.053162
percent_buy_market_BTC -0.108420 -0.006600 -0.052582 1.000000 0.999965
percent_buy_market_USDT -0.109291 -0.007067 -0.053162 0.999965 1.000000

Al estudiar las correlaciones entre las variables de volumen y las nuevas que representan el porcentaje de compras a mercado con respecto al volumen total, se observa que se ha diluido el problema de multicolinealidad entre ellas, ya que se pasa de unas correlaciones cuasi-perfectas a unas correlaciones negativas muy cercanas a 0. Pero ha surgido un problema de correlación perfecta entre las dos nuevas variables, algo que tiene sentido ya que, si bien la cantidad de Bitcoin y USD negociadas en un día no son iguales, los porcentajes que para ambas representan las compras a mercado con respecto de sus volúmenes sí son idénticos. De ahí que la correlación entre ambas nuevas variables sea prácticamente perfecta, por lo que se procederá a eliminar una de ellas y renombrar la otra como _percent_buymarket.

De esta manera, se mantiene la información aportada por las variables de _buymarket sobre el comportamiento relativo del volumen de compras a mercado y se elimina el problema de la multicolinealidad que existía debido a su alta correlación con las variables de volumen.

In [96]:
df.drop(columns=['percent_buy_market_USDT'], inplace=True)
df.rename(columns={'percent_buy_market_BTC': 'percent_buy_market'}, inplace=True)
df
Out[96]:
date close VOL_BTC VOL_USDT transactions total_supply fed_funds_rate sp500 eur_usd gbp_usd jpy_usd range delta_open_close percent_variation percent_buy_market
0 2017-08-17 4285.08 795.150377 3.454770e+06 3427 1.651956e+07 1.16 2430.010010 1.177426 1.289158 0.009091 284.65 -23.60 0.005538 0.775009
1 2017-08-18 4108.37 1199.888264 5.086958e+06 5233 1.651956e+07 1.16 2425.550049 1.171550 1.286505 0.009147 432.75 176.71 -0.041238 0.810799
2 2017-08-19 4139.98 381.309763 1.549484e+06 2153 1.651956e+07 1.16 2425.550049 1.172909 1.286792 0.009147 334.69 -31.61 0.007694 0.719457
3 2017-08-20 4086.29 467.083022 1.930364e+06 2321 1.651956e+07 1.16 2425.550049 1.174268 1.287079 0.009147 178.46 34.69 -0.008418 0.806700
4 2017-08-21 4016.00 691.743060 2.797232e+06 3972 1.651956e+07 1.16 2428.370117 1.175627 1.287366 0.009147 207.83 53.13 -0.013057 0.805727
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2654 2024-11-22 98892.00 46189.309243 4.555537e+09 7271311 1.978572e+07 4.58 5969.339844 1.046934 1.258479 0.006484 2465.90 -574.88 0.005847 0.491665
2655 2024-11-23 97672.40 24757.843670 2.431610e+09 3839138 1.978626e+07 4.58 5969.339844 1.047285 1.258780 0.006485 1772.85 1219.60 -0.012333 0.451423
2656 2024-11-24 97900.04 31200.978380 3.034172e+09 4964720 1.978680e+07 4.58 5969.339844 1.047636 1.259081 0.006486 2829.23 -227.64 0.002331 0.484732
2657 2024-11-25 93010.01 50847.450960 4.883445e+09 8289691 1.978734e+07 4.58 5987.370117 1.047987 1.259382 0.006487 6271.61 4890.04 -0.049949 0.461910
2658 2024-11-26 91965.16 57858.731380 5.370919e+09 10225809 1.978734e+07 4.58 6021.629883 1.044430 1.253306 0.006478 4182.27 1044.85 -0.011234 0.503299

2659 rows × 15 columns

Al tratarse del precio de un activo financiero, además de todas las variables vistas hasta el momento, existen una serie de indicadores técnicos muy empleados por los operadores de mercado para obtener información adicional sobre el precio, por lo que se procederá a crear dichos indicadores como variables y a comprobar si pueden ayudar al modelo a comprender mejor el comportamiento y a predecir de una manera más acertada el precio de Bitcoin.

Los indicadores que se calcularán serán: media móvil simple (sma), índice de fuerza relativa (rsi), bandas de bollinger (_bollingerupper y _bollingerlower), MACD (macd) y volatilidad histórica (_historicalvolatility).

  • Media Móvil Simple (SMA): es el promedio de precios en los últimos n días; la SMA ayuda a suavizar las fluctuaciones de precio, mostrando tendencias a corto y largo plazo. Se calcularán 2 SMA: la de 7 días, para mostrar la tendencia a corto plazo, y la de 20 días.
In [97]:
df['sma_7'] = sma(df)
df['sma_20'] = sma(df, 20)
  • Índice de fuerza relativa (RSI): oscilador que mide la velocidad y el cambio de los movimientos de precio, tomando valores entre 0 y 100. Puede identificar condiciones de sobrecompra o sobreventa cuando pasa de ciertos umbrales (70/30 generalmente), por lo que puede aportar información adicional al modelo sobre reversión de tendencias.
In [98]:
df['rsi'] = rsi(df)
  • Bandas de Bollinger: indicador que usa una SMA y dos bandas (una superior y otra inferior) calculadas como múltiplo de la desviación estándar. Ayudan a capturar la volatilidad del precio y posibles puntos de ruptura hacia arriba o hacia abajo del precio.
In [99]:
df['bollinger_upper'], df['bollinger_lower'] = bollinger_bands(df)
  • MACD (Moving Average Convergence Divergence): oscilador que mide la diferencia entre una media móvil exponencial rápida (EMA12) y una lenta (EMA26), e incluye una línea de señal. Indica cambios en la fuerza, dirección y duración de la tendencia.
In [100]:
df['macd'], df['signal_line'] = macd(df)
  • Volatilidad histórica: medida de la dispersión del precio como desviación estándar de los retornos logarítmicos. Ayuda a modelar el riesgo del activo, identificando periodos más estables o volátiles.
In [101]:
df['historical_volatility'] = historical_volatility(df)
In [102]:
df
Out[102]:
date close VOL_BTC VOL_USDT transactions total_supply fed_funds_rate sp500 eur_usd gbp_usd ... percent_variation percent_buy_market sma_7 sma_20 rsi bollinger_upper bollinger_lower macd signal_line historical_volatility
0 2017-08-17 4285.08 795.150377 3.454770e+06 3427 1.651956e+07 1.16 2430.010010 1.177426 1.289158 ... 0.005538 0.775009 NaN NaN NaN NaN NaN 0.000000 0.000000 NaN
1 2017-08-18 4108.37 1199.888264 5.086958e+06 5233 1.651956e+07 1.16 2425.550049 1.171550 1.286505 ... -0.041238 0.810799 NaN NaN NaN NaN NaN -14.096524 -2.819305 NaN
2 2017-08-19 4139.98 381.309763 1.549484e+06 2153 1.651956e+07 1.16 2425.550049 1.172909 1.286792 ... 0.007694 0.719457 NaN NaN NaN NaN NaN -22.458570 -6.747158 NaN
3 2017-08-20 4086.29 467.083022 1.930364e+06 2321 1.651956e+07 1.16 2425.550049 1.174268 1.287079 ... -0.008418 0.806700 NaN NaN NaN NaN NaN -33.037055 -12.005137 NaN
4 2017-08-21 4016.00 691.743060 2.797232e+06 3972 1.651956e+07 1.16 2428.370117 1.175627 1.287366 ... -0.013057 0.805727 NaN NaN NaN NaN NaN -46.555731 -18.915256 NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2654 2024-11-22 98892.00 46189.309243 4.555537e+09 7271311 1.978572e+07 4.58 5969.339844 1.046934 1.258479 ... 0.005847 0.491665 93530.494286 84551.5730 84.727434 104126.985845 64976.160155 6994.475759 6040.286951 0.032602
2655 2024-11-23 97672.40 24757.843670 2.431610e+09 3839138 1.978626e+07 4.58 5969.339844 1.047285 1.258780 ... -0.012333 0.451423 94542.705714 85996.3935 81.545267 104924.076928 67068.710072 7019.055192 6236.040599 0.032772
2656 2024-11-24 97900.04 31200.978380 3.034172e+09 4964720 1.978680e+07 4.58 5969.339844 1.047636 1.259081 ... 0.002331 0.484732 95691.855714 87498.8950 79.400401 105084.637135 69913.152865 6976.482666 6384.129013 0.032165
2657 2024-11-25 93010.01 50847.450960 4.883445e+09 8289691 1.978734e+07 4.58 5987.370117 1.047987 1.259382 ... -0.049949 0.461910 96055.560000 88680.7950 58.253704 104191.930064 73169.659936 6473.536184 6402.010447 0.035699
2658 2024-11-26 91965.16 57858.731380 5.370919e+09 10225809 1.978734e+07 4.58 6021.629883 1.044430 1.253306 ... -0.011234 0.503299 96006.184286 89500.4535 57.494634 103778.424835 75222.482165 5922.367445 6306.081847 0.031942

2659 rows × 23 columns

In [103]:
df[['close', 'sma_7', 'sma_20', 'rsi', 'bollinger_upper', 'bollinger_lower', 'macd', 
    'signal_line', 'historical_volatility']].corr()
Out[103]:
close sma_7 sma_20 rsi bollinger_upper bollinger_lower macd signal_line historical_volatility
close 1.000000 0.997428 0.990833 0.090884 0.990029 0.980983 0.307554 0.310510 -0.162180
sma_7 0.997428 1.000000 0.995764 0.056722 0.993946 0.987106 0.284278 0.299288 -0.160362
sma_20 0.990833 0.995764 1.000000 -0.004526 0.996017 0.993961 0.207282 0.236186 -0.153283
rsi 0.090884 0.056722 -0.004526 1.000000 0.009890 -0.022219 0.578921 0.443256 -0.113285
bollinger_upper 0.990029 0.993946 0.996017 0.009890 1.000000 0.980218 0.227827 0.252635 -0.112249
bollinger_lower 0.980983 0.987106 0.993961 -0.022219 0.980218 1.000000 0.179731 0.213359 -0.202105
macd 0.307554 0.284278 0.207282 0.578921 0.227827 0.179731 1.000000 0.961758 -0.106240
signal_line 0.310510 0.299288 0.236186 0.443256 0.252635 0.213359 0.961758 1.000000 -0.095938
historical_volatility -0.162180 -0.160362 -0.153283 -0.113285 -0.112249 -0.202105 -0.106240 -0.095938 1.000000
In [104]:
df[['close', 'sma_7', 'sma_20', 'rsi', 'bollinger_upper', 'bollinger_lower', 'macd', 
    'signal_line', 'historical_volatility']].corr('spearman')
Out[104]:
close sma_7 sma_20 rsi bollinger_upper bollinger_lower macd signal_line historical_volatility
close 1.000000 0.997504 0.991365 0.079953 0.988848 0.982785 0.206665 0.208557 -0.131333
sma_7 0.997504 1.000000 0.995806 0.043713 0.992648 0.987839 0.189582 0.201167 -0.130933
sma_20 0.991365 0.995806 1.000000 -0.024095 0.996039 0.992399 0.130636 0.154060 -0.126223
rsi 0.079953 0.043713 -0.024095 1.000000 -0.016045 -0.038707 0.680448 0.519558 -0.077607
bollinger_upper 0.988848 0.992648 0.996039 -0.016045 1.000000 0.979700 0.133550 0.154909 -0.076015
bollinger_lower 0.982785 0.987839 0.992399 -0.038707 0.979700 1.000000 0.117664 0.143454 -0.192104
macd 0.206665 0.189582 0.130636 0.680448 0.133550 0.117664 1.000000 0.948397 -0.088501
signal_line 0.208557 0.201167 0.154060 0.519558 0.154909 0.143454 0.948397 1.000000 -0.094106
historical_volatility -0.131333 -0.130933 -0.126223 -0.077607 -0.076015 -0.192104 -0.088501 -0.094106 1.000000

Del análisis de correlaciones entre las nuevas variables se destaca lo siguiente:

  • _sma7 y _sma20 están muy correlacionadas con close, algo lógico ya quederivan directamente de los precios de cierre, y, al mismo tiempo, tienen una alta correlación entre ambas, por lo que mantener las dos SMA podría generar redundancia. Se optará por mantener una de ellas para capturar la tendencia a corto plazo.
  • rsi tiene una baja correlación tanto lineal como monótona (ambas por debajo de 0.1). A pesar de que podría aportar información importante sobre momentos de sobrecompra o sobreventa (contexto clave en los mercados financieros), se decide su eliminación basada en este umbral. Se recomienda mantenerla en mente en caso de que el modelo necesite introducir nueva información para mejorar sus predicciones.
  • _bollingerupper y _bollingerlower están muy correlacionadas con close, pues de derivan directamente de la SMA y la desviación estándar del precio, pero, al mismo tiempo, están muy correlacionadas entre ellas, lo que puede resultar en redundancia. Se decide realizar una media de ambas que represente la volatilidad media en torno a la SMA, de manera que se mantiene la información y se evita la redundancia. En todo caso, las bandas de bollinger se encuentran a su vez muy correlacionadas con ambas SMA, pero se mantendrán las dos (_sma7 y _bollingermean, creada como media de ambas bandas de bollinger) ya que cada una aporta contexto distinto y la red neuronal puede ser capaz de ponderar adecuadamente la importancia de cada variable.
  • macd y _signalline tienen unas correlaciones moderadas con close y, al mismo tiempo, están muy correladas entre ellas, ya que la línea de señal se deriva directamente de MACD. Para poder capturar cambios de momentum y evitar al mismo tiempo la redundancia y la multicolinealidad, se decide mantener macd y eliminar _signalline.
  • _historicalvolatility tiene una correlación negativa bastante baja con close, pero está fuera del umbral establecido de entre 0.1 y -0.1, por lo que, para aportar información relevante sobre la estabilidad o el riesgo del precio, se decide mantener la variable.
In [105]:
df['bollinger_mean'] = (df['bollinger_upper'] + df['bollinger_lower']) / 2
df.drop(columns=['sma_20', 'rsi', 'bollinger_upper', 'bollinger_lower', 'signal_line'], inplace=True)
df
Out[105]:
date close VOL_BTC VOL_USDT transactions total_supply fed_funds_rate sp500 eur_usd gbp_usd jpy_usd range delta_open_close percent_variation percent_buy_market sma_7 macd historical_volatility bollinger_mean
0 2017-08-17 4285.08 795.150377 3.454770e+06 3427 1.651956e+07 1.16 2430.010010 1.177426 1.289158 0.009091 284.65 -23.60 0.005538 0.775009 NaN 0.000000 NaN NaN
1 2017-08-18 4108.37 1199.888264 5.086958e+06 5233 1.651956e+07 1.16 2425.550049 1.171550 1.286505 0.009147 432.75 176.71 -0.041238 0.810799 NaN -14.096524 NaN NaN
2 2017-08-19 4139.98 381.309763 1.549484e+06 2153 1.651956e+07 1.16 2425.550049 1.172909 1.286792 0.009147 334.69 -31.61 0.007694 0.719457 NaN -22.458570 NaN NaN
3 2017-08-20 4086.29 467.083022 1.930364e+06 2321 1.651956e+07 1.16 2425.550049 1.174268 1.287079 0.009147 178.46 34.69 -0.008418 0.806700 NaN -33.037055 NaN NaN
4 2017-08-21 4016.00 691.743060 2.797232e+06 3972 1.651956e+07 1.16 2428.370117 1.175627 1.287366 0.009147 207.83 53.13 -0.013057 0.805727 NaN -46.555731 NaN NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2654 2024-11-22 98892.00 46189.309243 4.555537e+09 7271311 1.978572e+07 4.58 5969.339844 1.046934 1.258479 0.006484 2465.90 -574.88 0.005847 0.491665 93530.494286 6994.475759 0.032602 84551.5730
2655 2024-11-23 97672.40 24757.843670 2.431610e+09 3839138 1.978626e+07 4.58 5969.339844 1.047285 1.258780 0.006485 1772.85 1219.60 -0.012333 0.451423 94542.705714 7019.055192 0.032772 85996.3935
2656 2024-11-24 97900.04 31200.978380 3.034172e+09 4964720 1.978680e+07 4.58 5969.339844 1.047636 1.259081 0.006486 2829.23 -227.64 0.002331 0.484732 95691.855714 6976.482666 0.032165 87498.8950
2657 2024-11-25 93010.01 50847.450960 4.883445e+09 8289691 1.978734e+07 4.58 5987.370117 1.047987 1.259382 0.006487 6271.61 4890.04 -0.049949 0.461910 96055.560000 6473.536184 0.035699 88680.7950
2658 2024-11-26 91965.16 57858.731380 5.370919e+09 10225809 1.978734e+07 4.58 6021.629883 1.044430 1.253306 0.006478 4182.27 1044.85 -0.011234 0.503299 96006.184286 5922.367445 0.031942 89500.4535

2659 rows × 19 columns

In [106]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2659 entries, 0 to 2658
Data columns (total 19 columns):
 #   Column                 Non-Null Count  Dtype         
---  ------                 --------------  -----         
 0   date                   2659 non-null   datetime64[ns]
 1   close                  2659 non-null   float64       
 2   VOL_BTC                2659 non-null   float64       
 3   VOL_USDT               2659 non-null   float64       
 4   transactions           2659 non-null   int64         
 5   total_supply           2659 non-null   float64       
 6   fed_funds_rate         2659 non-null   float64       
 7   sp500                  2659 non-null   float64       
 8   eur_usd                2659 non-null   float64       
 9   gbp_usd                2659 non-null   float64       
 10  jpy_usd                2659 non-null   float64       
 11  range                  2659 non-null   float64       
 12  delta_open_close       2659 non-null   float64       
 13  percent_variation      2659 non-null   float64       
 14  percent_buy_market     2659 non-null   float64       
 15  sma_7                  2653 non-null   float64       
 16  macd                   2659 non-null   float64       
 17  historical_volatility  2639 non-null   float64       
 18  bollinger_mean         2640 non-null   float64       
dtypes: datetime64[ns](1), float64(17), int64(1)
memory usage: 394.8 KB

Una vez creada las nuevas variables basadas en indicadores técnicos financieros, se advierte que existen valores nulos para algunas de ellas, al estar basadas en medias de varios días atrás. Por ejemplo, para la SMA de 7 días se necesitan los datos de los 6 días anteriores más el día en cuestión, por lo que los 6 primeros días del dataset no tendrán dato para _sma7 al no contar con información suficiente para calcular la media. Al tratarse de las 20 primeras observaciones, y dado que la imputación mediante interpolación forward fill (que sería la que habría que usar en este caso) puede introducir suposiciones que podrían no reflejar el comportamiento real de las variables, se opta en esta ocasión por eliminar dichas observaciones.

In [107]:
df.dropna(inplace=True)
df.reset_index(drop=True, inplace=True)
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2639 entries, 0 to 2638
Data columns (total 19 columns):
 #   Column                 Non-Null Count  Dtype         
---  ------                 --------------  -----         
 0   date                   2639 non-null   datetime64[ns]
 1   close                  2639 non-null   float64       
 2   VOL_BTC                2639 non-null   float64       
 3   VOL_USDT               2639 non-null   float64       
 4   transactions           2639 non-null   int64         
 5   total_supply           2639 non-null   float64       
 6   fed_funds_rate         2639 non-null   float64       
 7   sp500                  2639 non-null   float64       
 8   eur_usd                2639 non-null   float64       
 9   gbp_usd                2639 non-null   float64       
 10  jpy_usd                2639 non-null   float64       
 11  range                  2639 non-null   float64       
 12  delta_open_close       2639 non-null   float64       
 13  percent_variation      2639 non-null   float64       
 14  percent_buy_market     2639 non-null   float64       
 15  sma_7                  2639 non-null   float64       
 16  macd                   2639 non-null   float64       
 17  historical_volatility  2639 non-null   float64       
 18  bollinger_mean         2639 non-null   float64       
dtypes: datetime64[ns](1), float64(17), int64(1)
memory usage: 391.9 KB

Para finalizar la preparación de los datos, se procederá a la normalización de los mismos. Este paso es importante para ayudar a aquellos modelos sensibles a la escala (como las redes neuronales recurrentes) a que estimen bien el precio de Bitcoin. Se seguirán dos estrategias distintas de normalización, seleccionando una serie de variables que serán normalizadas en ventanas de tiempo y otras que lo serán de manera global. La normalización en ventanas de tiempo es útil en series temporales para reflejar los cambios dinámicos en el contexto de las variables, previniendo fugas de información del futuro al normalizar con datos muy anteriores y capturando patrones locales en lugar de a una escala global que podría no ser representativa en el largo plazo.

  • close: la variable target será normalizada en ventanas de tiempo para garantizar que las predicciones sean relativas a valores recientes. Sin embargo, hay que tener presente que los resultados que arroje el modelo deberán ser desescalados para poder interpretarlos correctamente.
  • _VOLBTC, _VOLUSDT, transactions, range, _delta_openclose, _percentvariation y _percent_buymarket: serán normalizadas en ventanas de tiempo dado que volúmenes y transacciones cambian dinámicamente y se puede ayudar de esta manera a capturar patrones contextuales.
  • sp500, _eurusd, _gbpusd y _jpyusd: serán normalizadas en ventanas de tiempo dado que los indicadores macroeconómicos pueden tener tendencias locales relevantes.
  • _sma7, macd, _historicalvolatility y _bollingermean: serán normalizadas en ventanas de tiempo, si bien para cada una de ellas habrá que hacerlo dentro de las mismas ventanas de tiempo en las que son calculados.
  • _totalsupply y _fed_fundsrate: serán normalizadas globalmente ya que no tienen un contexto dinámico local.
In [108]:
norm_20 = ['close', 'VOL_BTC', 'VOL_USDT', 'transactions', 'percent_buy_market', 'sp500', 'eur_usd', 'gbp_usd', 'jpy_usd', 
           'sma_7', 'macd', 'historical_volatility', 'bollinger_mean']
norm_7 = ['range', 'delta_open_close', 'percent_variation']
norm_global = ['total_supply', 'fed_funds_rate']
In [109]:
df_normalized = df.copy()

for var in norm_global:
    df_normalized[var] = normalize_zscore(df, var)

for var in norm_7:
    df_normalized[var] = normalize_zscore(df, var, _window=7)
    
for var in norm_20:
    df_normalized[var] = normalize_zscore(df, var, _window=20)

df_normalized
Out[109]:
date close VOL_BTC VOL_USDT transactions total_supply fed_funds_rate sp500 eur_usd gbp_usd jpy_usd range delta_open_close percent_variation percent_buy_market sma_7 macd historical_volatility bollinger_mean
0 2017-09-06 0.000000 0.000000 0.000000 0.000000 -2.123873 -0.559575 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
1 2017-09-07 1.000000 -1.000000 -1.000000 -1.000000 -2.121590 -0.559575 -1.000000 1.000000 1.000000 -1.000000 1.000000 1.000000 -1.000000 -1.000000 -1.000000 1.000000 -1.000000 1.000000
2 2017-09-08 -1.394931 1.236182 1.211733 -0.256371 -2.119306 -0.559575 -1.407479 1.413532 1.403426 1.122117 1.412178 1.363598 -1.355800 -1.309103 -1.412401 -1.023541 1.373680 0.922188
3 2017-09-09 -1.052193 -0.497055 -0.608152 -0.592224 -2.117023 -0.559575 -0.996818 0.961739 1.352940 0.868416 -0.814725 -0.012162 -0.015227 -0.612001 -1.283470 -1.464938 0.960669 1.026464
4 2017-09-10 -1.216382 -0.428307 -0.618447 -0.655099 -2.114739 -0.559575 -0.814330 0.737161 1.384527 0.750490 0.345394 0.368817 -0.420229 -0.438382 -1.456822 -1.644020 0.895415 1.042165
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2634 2024-11-22 1.503209 -0.189959 0.107295 0.137776 1.381010 1.195509 0.643943 -1.478081 -1.484743 -0.194169 -0.718971 0.360430 -0.379454 -0.735712 1.485522 1.338367 0.543889 1.933734
2635 2024-11-23 1.265800 -1.054117 -0.908510 -1.245873 1.381595 1.195509 0.575544 -1.321903 -1.311802 -0.106426 -1.485190 1.350555 -1.327074 -2.729985 1.436747 1.226510 0.497017 1.908522
2636 2024-11-24 1.213637 -0.798832 -0.661910 -0.780186 1.382181 1.195509 0.532303 -1.190516 -1.167351 -0.014875 -0.359405 0.599217 -0.622036 -0.801017 1.414607 1.107554 0.288867 1.889185
2637 2024-11-25 0.572709 -0.040178 0.170503 0.487972 1.382766 1.195509 0.854844 -1.080196 -1.041109 0.087710 1.875818 1.997675 -1.981597 -1.777760 1.329796 0.773773 1.167842 1.827903
2638 2024-11-26 0.354215 0.391267 0.499192 1.196128 1.382766 1.195509 1.447457 -1.366222 -1.356228 0.019515 0.344444 0.384151 -0.409982 0.357434 1.220564 0.381991 -0.042660 1.731872

2639 rows × 19 columns

Debido a que la normalización en ventanas de tiempo requiere conocer datos hacia atrás para el cálculo de la media y la desviación (es decir, requieren de una ventana completa de x días hacia atrás), las primeras observaciones tienen valores 0, 1 y -1 para muchas de sus variables normalizadas, lo que puede llevar a problemas de consistencia en los datos e introducir un pequeño sesgo en los modelos a entrenar, pues sus estadísticas no son comparables al resto. Por este motivo, se decide eliminar estas observaciones.

Dado que la ventana máxima que se ha empleado en todas las normalizaciones es de 20 días, las primeras 19 observaciones no cuentan con ventanas completas para realizar la normalización de las variables que se han normalizado con ventanas de 20 días, por lo que sus datos no son consistentes y serán las observaciones eliminadas.

In [110]:
df_ready = df_normalized.iloc[19:].reset_index(drop=True).copy()

df_ready
Out[110]:
date close VOL_BTC VOL_USDT transactions total_supply fed_funds_rate sp500 eur_usd gbp_usd jpy_usd range delta_open_close percent_variation percent_buy_market sma_7 macd historical_volatility bollinger_mean
0 2017-09-25 -0.085401 -0.560892 -0.699981 1.261409 -2.082601 -0.559575 0.350046 -0.592119 0.824893 -1.274712 1.144172 -1.577317 1.589879 -0.755249 -0.949703 -0.641271 0.795666 -1.846998
1 2017-09-26 -0.099713 -1.063747 -1.215896 0.447806 -2.080645 -0.559575 0.284031 -1.957006 0.592814 -0.768540 -1.602020 0.266043 -0.277257 -0.444980 -0.878831 -0.397677 0.623934 -1.794520
2 2017-09-27 1.122924 -0.814737 -0.818137 0.556466 -2.078689 -0.559575 0.860033 -2.620322 0.344516 -1.046032 1.299942 -1.331452 1.283955 1.242939 -0.654669 0.168071 0.705831 -1.695572
3 2017-09-28 1.102849 -0.152215 0.091193 1.820999 -2.076600 -0.559575 1.025069 -2.568349 -0.052404 -1.236413 -0.561697 0.645138 -0.645081 -0.483257 -0.259479 0.715206 0.429499 -1.530909
4 2017-09-29 1.142915 1.723521 2.239944 2.727527 -2.074512 -0.610893 1.800645 -1.846679 0.064926 -0.909286 0.671190 0.550353 -0.559231 -0.690897 0.201619 1.241301 0.348591 -1.394947
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2615 2024-11-22 1.503209 -0.189959 0.107295 0.137776 1.381010 1.195509 0.643943 -1.478081 -1.484743 -0.194169 -0.718971 0.360430 -0.379454 -0.735712 1.485522 1.338367 0.543889 1.933734
2616 2024-11-23 1.265800 -1.054117 -0.908510 -1.245873 1.381595 1.195509 0.575544 -1.321903 -1.311802 -0.106426 -1.485190 1.350555 -1.327074 -2.729985 1.436747 1.226510 0.497017 1.908522
2617 2024-11-24 1.213637 -0.798832 -0.661910 -0.780186 1.382181 1.195509 0.532303 -1.190516 -1.167351 -0.014875 -0.359405 0.599217 -0.622036 -0.801017 1.414607 1.107554 0.288867 1.889185
2618 2024-11-25 0.572709 -0.040178 0.170503 0.487972 1.382766 1.195509 0.854844 -1.080196 -1.041109 0.087710 1.875818 1.997675 -1.981597 -1.777760 1.329796 0.773773 1.167842 1.827903
2619 2024-11-26 0.354215 0.391267 0.499192 1.196128 1.382766 1.195509 1.447457 -1.366222 -1.356228 0.019515 0.344444 0.384151 -0.409982 0.357434 1.220564 0.381991 -0.042660 1.731872

2620 rows × 19 columns

3.4. Reducción de dimensionalidad¶

Debido a que 17 variables predictivas no representan una complejidad excesiva para una red neuronal, se decide no realizar una reducción de dimensionalidad, al menos por el momento. Una vez entrenado el modelo principal, en función de sus resultados se tomará la decisión de si reducir la dimensionalidad o no de cara al entrenamiento de modelos más simples con los que comparar resultados, para lo que se emplearán los valores de SHAP (Shapley Additive Explanations), que evalúan la importancia de las características en función del impacto que han tenido en las predicciones del modelo.

Por lo tanto, el dataset final, una vez realizadas todas las transformaciones, eliminaciones, adiciones y normalizaciones, consta de 2619 observaciones, 17 variables explicativas, 1 variable fecha y la variable objetivo.

In [111]:
# Se guardan en formato csv el dataframe que contiene todos los datos preparados para entrenar el modelo
save_csv(df_ready, "df", "ready")

'''Se guarda en formato csv un dataframe con los precios de cierre de Bitcoin; estos datos servirán posteriormente 
para desnormalizar los valores predichos por el modelo, ya que, al haber sido normalizados en ventanas de tiempo, 
no es posible emplear una media y una desviación únicas para todos los datos.
También se empleará para comparar los valores predichos con los valores reales.'''
save_csv(df.close[19:], "df", "close")